Compare commits

...

86 commits
v0.1.4 ... main

Author SHA1 Message Date
Saroj Regmi
ac903e80b3
docs: updated MAKING_THEMS.md (#86)
Some checks failed
Build Hyprland / build (push) Has been cancelled
Test / nix (push) Has been cancelled
* docs: updated MAKING_THEMS.md 

Added a link to find all cursor names that the server may request.

* docs: updated the cursor name list reference and clarified about it.
2025-04-29 19:40:57 +02:00
Vaxry
2fd36421c2 README: remove todos 2025-03-17 12:46:18 +00:00
Vaxry
028bedbc63 version: bump to 0.1.12 2025-03-17 12:44:33 +00:00
vaxerski
7c6d165e1e meta: fix hyprlang colon handling
fixes #80
2025-02-04 10:29:10 +00:00
Honkazel
0a8e83d35b
core: clang-tidy and comp fixes (#79)
* clang-tidy and comp fixes

* oops
2025-02-03 20:49:19 +01:00
Honkazel
43e5139076
cmake: remove clang workaround (#78) 2025-02-02 19:43:23 +01:00
Asromo
dcadd3398a
core: fix memory leak by freeing rsvg handle (#77) 2025-01-29 20:17:35 +01:00
Mihai Fufezan
9c5dd1f7c8
flake.lock: update 2025-01-23 14:22:17 +02:00
Drewry Pope
3219b31128
tests: fix %d->%ld (#76) 2025-01-20 16:38:36 +00:00
Mihai Fufezan
69270ba8f0
flake.lock: update 2024-12-23 00:29:00 +02:00
Vaxry
3b3259e52a version: bump to 0.1.11 2024-12-21 20:06:59 +00:00
Vaxry
abc1c60eb5 meta: clamp nominalSize 2024-12-21 20:00:02 +00:00
Vaxry
84203d8126
core: add nominal size support (#73)
Adds nominal size support
2024-12-21 20:31:12 +01:00
Mihai Fufezan
f388aacd22
flake.lock: update
overlays: gcc13Stdenv -> gcc14Stdenv
2024-12-16 17:58:29 +02:00
Jan Beich
c18572a92e
util: add missing header for libc++ (#71)
hyprcursor-util/src/main.cpp:260:19: error: implicit instantiation of undefined template 'std::basic_ofstream<char>'
  260 |     std::ofstream manifest(out + "/manifest.hl", std::ios::trunc);
      |                   ^
/usr/include/c++/v1/__fwd/fstream.h:26:28: note: template is declared here
   26 | class _LIBCPP_TEMPLATE_VIS basic_ofstream;
      |                            ^
hyprcursor-util/src/main.cpp:292:41: error: implicit instantiation of undefined template 'std::basic_ifstream<char>'
  292 |         std::ifstream                   xconfig("/tmp/hyprcursor-util/" + xcursor.path().stem().string() + ".conf");
      |                                         ^
/usr/include/c++/v1/__fwd/fstream.h:24:28: note: template is declared here
   24 | class _LIBCPP_TEMPLATE_VIS basic_ifstream;
      |                            ^
hyprcursor-util/src/main.cpp:370:23: error: implicit instantiation of undefined template 'std::basic_ofstream<char>'
  370 |         std::ofstream meta(CURSORDIR + "/meta.hl", std::ios::trunc);
      |                       ^
/usr/include/c++/v1/__fwd/fstream.h:26:28: note: template is declared here
   26 | class _LIBCPP_TEMPLATE_VIS basic_ofstream;
      |                            ^
2024-12-14 14:05:23 +01:00
Vaxry
0264e69814 core: allow ;-separated values in hl format
fixes #67
2024-10-11 19:02:18 +01:00
Mihai Fufezan
70fb494aa6
CI: add test action 2024-10-08 23:47:22 +03:00
Mihai Fufezan
572cb49bb7
Nix: add hyprcursor-with-tests 2024-10-08 23:45:47 +03:00
Mihai Fufezan
53a23e4b41
CMake: allow installing tests 2024-10-08 23:44:50 +03:00
Vaxry
d60e1e01e6 tests: fixup C test override checking 2024-10-01 23:26:44 +01:00
Vaxry
5729b9733d core: fixup overridenBy reporting 2024-10-01 23:26:36 +01:00
Jan Beich
34efe230c2
core: add missing header for libc++ after 5a95d8512b (#66)
libhyprcursor/hyprcursor.cpp:23:27: error: implicit instantiation of undefined template 'std::basic_stringstream<char>'
   23 |         std::stringstream envXdgStream(envXdgData);
      |                           ^
/usr/include/c++/v1/__fwd/sstream.h:29:28: note: template is declared here
   29 | class _LIBCPP_TEMPLATE_VIS basic_stringstream;
      |                            ^
2024-10-01 10:08:26 +01:00
Vaxry
704cd7fed0 version: bump to 0.1.10 2024-09-30 22:25:02 +01:00
Vaxry
e8acfdb903 gitignore: add .cache 2024-09-30 18:29:41 +01:00
Vaxry
6b4131ee52 core: initialize C shape data fully 2024-09-30 18:12:34 +01:00
Vaxry
66648429bd core: avoid uninitialized overriddenBy of raw shape data
ref #64
2024-09-30 18:10:29 +01:00
Pascal Lasnier
b98726e431
docs: Correction in hotspot coordinates documentation (#63) 2024-09-28 15:13:23 +01:00
Jacob Birkett
912d56025f nix: pkg: add missing dep xcur2png 2024-08-02 21:24:31 +03:00
Libadoxon
5a95d8512b
lib: Use XDG_DATA_DIRS to query themes (#58)
* lib: use XDG_DATA_DIRS to search themes

* lib: fix some stylistic errors

* lib: more stylistic errors fixed
2024-07-30 22:04:10 +02:00
Mihai Fufezan
4493a972b4
flake.lock: update 2024-07-18 22:19:31 +03:00
Mihai Fufezan
04cb4df80a
CMake: fmt 2024-07-18 22:18:46 +03:00
Mihai Fufezan
4efcbd36a2
CMake, Nix: add VERSION file 2024-07-18 22:18:35 +03:00
Ikalco
a5c0d57325
core: only alloc as much as needed when reading in cursor images (#51) 2024-07-04 17:59:59 +02:00
Vaxry
66d5b46ff9
README: add standards link 2024-06-15 13:24:35 +02:00
Vaxry
dd3a853c82 README: add toml++ to dep list 2024-06-14 14:32:02 +02:00
Vaxry
c50b2f0f1d ci: fix missing dep 2024-06-14 14:31:45 +02:00
Vaxry
d5f4a6c708 docs: mention timeouts to be > 0 2024-06-13 12:11:39 +02:00
Visual-Dawg
9e27a2c2ce
README: add wiki (#45) 2024-05-31 21:55:20 +03:00
vaxerski
57298fc4f1 cmake: bump ver to 0.1.9 2024-05-24 20:46:51 +02:00
Ikalco
27ca640abe
core/API: add option to not use default fallbacks (env and first available) (#43)
* add option to not use default fallbacks (env and first available)
2024-05-21 23:45:11 +02:00
Vaxry
7c3aa03dff shapes: fix nearest size finding for png cursors
fixes #14
2024-05-15 17:50:17 +01:00
Vaxry
dfba774650 cmake: bump ver to 0.1.8 2024-05-15 17:50:17 +01:00
Daniel Horton
4a32d0cf25
README: Fixed getconf command in build instructions (#42)
getconf NPROCESSORS_CONF isn't a valid command. The correct command is getconf _NPROCESSORS_CONF.
2024-05-14 16:14:13 +01:00
Eric Leblond
cab4746180
zip: fix build for some distros (#37) (#38)
libzip 1.10.1 is not available on some distributions. This patch
introduces a workaround to fix the build instead of jumping to
1.10.1 release.
2024-04-20 12:23:33 +01:00
Mihai Fufezan
0a53b9957f
flake.lock: update 2024-04-15 23:54:23 +03:00
vaxerski
c38dcf160d util: fix printing overrides
ref #36
2024-04-15 14:21:28 +01:00
Vaxry
1f4c960cf1 cmake: require libzip>=1.10.1
fixes #35
2024-04-14 19:33:07 +01:00
SoSeDiK
92af141a01
util: Minor cleanup (#34)
Avoid double // in paths
Replace magic 0 value with libzip's ZIP_LENGTH_TO_END
Correct png to image (can be svg as well)
2024-04-12 11:55:59 +01:00
SoSeDiK
d41e8ac8d1
zip: Properly report error on zip_close (#33) 2024-04-12 11:54:39 +01:00
SoSeDiK
178717746d
lib: Add validation for cursor file names and propagate the error from parsing HL cursor (#32)
* Validate cursor file names

* Propagate errors from parsing HL cursor

* Validate cursor directory names
2024-04-12 01:01:33 +01:00
SoSeDiK
f6a6322a03
lib: Count cursor-less themes as invalid (#31) 2024-04-10 17:29:25 +01:00
Vaxry
6742e9d3e2 props: bump ver to 0.1.7 2024-04-09 16:30:19 +01:00
Jin Liu
e5e3d140a4
docs: specify that pixel coordinates of hotspot are rounded to the nearest (#28)
Since we now have a raw API, we shall specify how this is calculated, so
users of the raw API can do the same in their rendering code.
2024-04-09 02:29:44 +01:00
Vaxry
17ebb7fff0 lib: add missing header
ref #30
2024-04-08 23:31:11 +01:00
Vaxry
bd56398f19 lib: round hotspots in getShapes
ref #28
2024-04-08 18:23:14 +01:00
Vaxry
af4ce3953d lib: minor manifest reading fixes
fixes #29
2024-04-08 18:20:10 +01:00
vaxerski
65507c093f lib: fix missing overrides
ref #27
2024-04-08 11:46:28 +01:00
Vaxry
033416cedc util: pack meta with the correct extension
fixes #25
2024-04-08 10:23:45 +01:00
Vaxry
95cd9376e8 lib: fix missing / in path 2024-04-08 10:21:12 +01:00
Vaxry
818d8c4b69 cmake: bump ver to 0.1.6 2024-04-06 21:18:01 +01:00
Vaxry
7561459770
lib: Added a raw data API (#23)
* raw data API

* add missing type arg

* tests: better stuff
2024-04-06 21:16:55 +01:00
Mihai Fufezan
981b661782
CI: add tomlplusplus 2024-04-05 20:50:58 +03:00
Mihai Fufezan
08fbf37b1c
Merge pull request #22 from fred21O4/patch-1
fix missing follow in flake.nix
2024-04-05 20:45:35 +03:00
fred21O4
7266c021cd
fix missing follow in flake.nix
this causes an extra instance of the systems flake, and makes it very difficult to overide in upstream flakes
2024-04-05 11:00:30 +13:00
Mihai Fufezan
6b1dc5e15a
Nix: add tomlplusplus dep 2024-04-04 20:01:46 +03:00
Vaxry
d780013ffa CMake: move hyprcursor-util to parent 2024-04-04 17:40:42 +01:00
Vaxry
aaccfdc83d docs: add toml instructions 2024-04-04 16:27:39 +01:00
Vaxry
4781252877 tests: fixup hardcoded themes as always 2024-04-04 16:22:14 +01:00
Vaxry
f4ea0297a0 core: Add support for toml manifests and metas
ref #20
2024-04-04 16:21:38 +01:00
Mihai Fufezan
be7e9f93cf
Nix: patch in search dir 2024-04-04 08:48:42 +03:00
Maximilian Seidler
752cc44779
docs: mention aspect ratio of cursors (#18) 2024-04-02 15:35:03 +01:00
vaxerski
73721de9ae lib: fixup path accessibility lookups 2024-04-02 15:28:31 +01:00
vaxerski
d3876f3477 lib: improve access checks on themes 2024-03-26 15:26:26 +00:00
Vaxry
1a1fcfb58d lib: fixup theme name matching 2024-03-25 11:54:29 +00:00
Vaxry
44d46e45a1 tests: comment the functionality 2024-03-25 01:45:46 +00:00
Vaxry
75751ed957 headers: fixup since vers 2024-03-24 20:50:26 +00:00
Vaxry
22a4195557 lib: add user-defined logging 2024-03-24 20:37:31 +00:00
Vaxry
f870f0f980 cmake: bump ver to 0.1.5 2024-03-24 03:23:53 +00:00
Vaxry
7bd0d55aa2 lib: set size to 0 for svg images
ref #13
2024-03-23 01:04:53 +00:00
Vaxry
6a92473237 lib: accept theme names for lookup 2024-03-21 15:42:22 +00:00
Zach DeCook
8a874fc49c
tests: Prevent tests from crashing when manager is invalid (#9) 2024-03-21 15:21:43 +00:00
Rudolchr
e3694ecf2f
cmake: Fix clang build by adjusting passed flags (#11)
* Just add -std=gnu++2b to clang++ or clang will error out

* Assuming it was meant to silence the __cpp_concepts warning that appears by passing it via flags
2024-03-18 16:48:04 +00:00
Vaxry
4b9efbed7a util: wrap paths in quotes in shell invocations
ref #10
2024-03-17 00:29:10 +00:00
solopasha
59acebef20
cmake: correct includedir permissions (#7)
no need to set 777
2024-03-16 17:03:39 +00:00
Vaxry
60f9c53cf2 lib: avoid arithmetic on void*
fixes #8
2024-03-16 16:39:14 +00:00
Vaxry
1761f6cefd cmake: require hyprlang 0.4.2 2024-03-12 15:29:19 +00:00
34 changed files with 1643 additions and 563 deletions

101
.clang-tidy Normal file
View file

@ -0,0 +1,101 @@
WarningsAsErrors: '*'
HeaderFilterRegex: '.*\.hpp'
FormatStyle: file
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace,
-bugprone-forward-declararion-namespace,
-bugprone-macro-parentheses,
-bugprone-narrowing-conversions,
-bugprone-branch-clone,
-bugprone-assignment-in-if-condition,
concurrency-*,
-concurrency-mt-unsafe,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-explicit-virtual-functions,
-cppcoreguidelines-avoid-c-arrays,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-narrowing-conversions,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-reinterpret-cast,
google-global-names-in-headers,
-google-readability-casting,
google-runtime-operator,
misc-*,
-misc-unused-parameters,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-include-cleaner,
-misc-use-anonymous-namespace,
-misc-const-correctness,
modernize-*,
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
-modernize-use-using,
-modernize-use-override,
-modernize-avoid-c-arrays,
-modernize-macro-to-enum,
-modernize-loop-convert,
-modernize-use-nodiscard,
-modernize-pass-by-value,
-modernize-use-auto,
performance-*,
-performance-avoid-endl,
-performance-unnecessary-value-param,
portability-std-allocator-const,
readability-*,
-readability-function-cognitive-complexity,
-readability-function-size,
-readability-identifier-length,
-readability-magic-numbers,
-readability-uppercase-literal-suffix,
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
-readability-redundant-member-init,
-readability-redundant-string-init,
-readability-avoid-const-params-in-decls,
-readability-named-parameter,
-readability-convert-member-functions-to-static,
-readability-qualified-auto,
-readability-make-member-function-const,
-readability-isolate-declaration,
-readability-inconsistent-declaration-parameter-name,
-clang-diagnostic-error,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true
performance-inefficient-string-concatenation.StrictMode: true
readability-braces-around-statements.ShortStatementLines: 0
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.ClassIgnoredRegexp: I.*
readability-identifier-naming.ClassPrefix: C # We can't use regex here?!?!?!?
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.EnumPrefix: e
readability-identifier-naming.EnumConstantCase: UPPER_CASE
readability-identifier-naming.FunctionCase: camelBack
readability-identifier-naming.NamespaceCase: CamelCase
readability-identifier-naming.NamespacePrefix: N
readability-identifier-naming.StructPrefix: S
readability-identifier-naming.StructCase: CamelCase

View file

@ -16,7 +16,11 @@ jobs:
run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang cairo librsvg git libzip
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang cairo librsvg git libzip tomlplusplus
- name: Get hyprutils-git
run: |
git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build
- name: Install hyprlang
run: |

39
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: Test
on: [push, pull_request, workflow_dispatch]
jobs:
nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
# not needed (yet)
# - uses: cachix/cachix-action@v12
# with:
# name: hyprland
# authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build
run: nix build .#hyprcursor-with-tests --print-build-logs --keep-going
# keep a fixed rev in case anything changes
- name: Install hyprcursor theme
run: nix build github:fufexan/dotfiles/4e05e373c1c70a2ae259b2c15eec2ad6e11ce581#bibata-hyprcursor --print-build-logs --keep-going
- name: Set up env
run: |
export HYPRCURSOR_THEME=Bibata-Modern-Classic-Hyprcursor
export HYPRCURSOR_SIZE=16
mkdir -p $HOME/.local/share/icons
ln -s $(realpath result/share/icons/Bibata-Modern-Classic-Hyprcursor) $HOME/.local/share/icons/
- name: Run test1
run: nix shell .#hyprcursor-with-tests -c hyprcursor_test1
- name: Run test2
run: nix shell .#hyprcursor-with-tests -c hyprcursor_test2
- name: Run test_c
run: nix shell .#hyprcursor-with-tests -c hyprcursor_test_c

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
.vscode/
build/
.cache/

View file

@ -1,12 +1,14 @@
cmake_minimum_required(VERSION 3.19)
set(HYPRCURSOR_VERSION "0.1.4")
file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW)
string(STRIP ${VER_RAW} HYPRCURSOR_VERSION)
add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}")
project(hyprcursor
VERSION ${HYPRCURSOR_VERSION}
DESCRIPTION "A library and toolkit for the Hyprland cursor format"
)
project(
hyprcursor
VERSION ${HYPRCURSOR_VERSION}
DESCRIPTION "A library and toolkit for the Hyprland cursor format")
include(CTest)
include(GNUInstallDirs)
@ -18,60 +20,111 @@ set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR})
configure_file(hyprcursor.pc.in hyprcursor.pc @ONLY)
set(CMAKE_CXX_STANDARD 23)
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Wno-unused-parameter
-Wno-unused-value
-Wno-missing-field-initializers
-Wno-narrowing
-Wno-pointer-arith)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.0 libzip cairo librsvg-2.0)
pkg_check_modules(
deps
REQUIRED
IMPORTED_TARGET
hyprlang>=0.4.2
libzip
cairo
librsvg-2.0
tomlplusplus)
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring hyprcursor in Debug")
add_compile_definitions(HYPRLAND_DEBUG)
message(STATUS "Configuring hyprcursor in Debug")
add_compile_definitions(HYPRLAND_DEBUG)
else()
add_compile_options(-O3)
message(STATUS "Configuring hyprcursor in Release")
add_compile_options(-O3)
message(STATUS "Configuring hyprcursor in Release")
endif()
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "libhyprcursor/*.cpp" "include/hyprcursor/hyprcursor.hpp" "include/hyprcursor/hyprcursor.h" "include/hyprcursor/shared.h")
file(
GLOB_RECURSE
SRCFILES
CONFIGURE_DEPENDS
"libhyprcursor/*.cpp"
"include/hyprcursor/hyprcursor.hpp"
"include/hyprcursor/hyprcursor.h"
"include/hyprcursor/shared.h")
add_library(hyprcursor SHARED ${SRCFILES})
target_include_directories( hyprcursor
PUBLIC "./include"
PRIVATE "./libhyprcursor"
)
set_target_properties(hyprcursor PROPERTIES
VERSION ${hyprcursor_VERSION}
SOVERSION 0
PUBLIC_HEADER include/hyprcursor/hyprcursor.hpp include/hyprcursor/hyprcursor.h include/hyprcursor/shared.h
)
target_include_directories(
hyprcursor
PUBLIC "./include"
PRIVATE "./libhyprcursor")
set_target_properties(
hyprcursor
PROPERTIES VERSION ${hyprcursor_VERSION}
SOVERSION 0
PUBLIC_HEADER include/hyprcursor/hyprcursor.hpp
include/hyprcursor/hyprcursor.h include/hyprcursor/shared.h)
target_link_libraries(hyprcursor PkgConfig::deps)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# for std::expected.
# probably evil. Arch's clang is very outdated tho...
target_compile_options(hyprcursor PUBLIC -std=gnu++2b -D__cpp_concepts=202002L -Wno-macro-redefined)
endif()
# hyprcursor-util
add_subdirectory(hyprcursor-util)
install(TARGETS hyprcursor)
file(
GLOB_RECURSE
UTILSRCFILES
CONFIGURE_DEPENDS
"hyprcursor-util/src/*.cpp"
"include/hyprcursor/hyprcursor.hpp"
"include/hyprcursor/hyprcursor.h"
"include/hyprcursor/shared.h")
add_executable(hyprcursor-util ${UTILSRCFILES})
target_include_directories(
hyprcursor-util
PUBLIC "./include"
PRIVATE "./libhyprcursor" "./hyprcursor-util/src")
target_link_libraries(hyprcursor-util PkgConfig::deps hyprcursor)
# tests
add_custom_target(tests)
add_executable(hyprcursor_test "tests/test.cpp")
target_link_libraries(hyprcursor_test PRIVATE hyprcursor)
add_test(NAME "Test libhyprcursor in C++" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test)
add_dependencies(tests hyprcursor_test)
add_executable(hyprcursor_test1 "tests/full_rendering.cpp")
target_link_libraries(hyprcursor_test1 PRIVATE hyprcursor)
add_test(
NAME "Test libhyprcursor in C++ (full rendering)"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprcursor_test1)
add_dependencies(tests hyprcursor_test1)
add_executable(hyprcursor_test_c "tests/test.c")
add_executable(hyprcursor_test2 "tests/only_metadata.cpp")
target_link_libraries(hyprcursor_test2 PRIVATE hyprcursor)
add_test(
NAME "Test libhyprcursor in C++ (only metadata)"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprcursor_test2)
add_dependencies(tests hyprcursor_test2)
add_executable(hyprcursor_test_c "tests/c_test.c")
target_link_libraries(hyprcursor_test_c PRIVATE hyprcursor)
add_test(NAME "Test libhyprcursor in C" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test_c)
add_test(
NAME "Test libhyprcursor in C"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprcursor_test_c)
add_dependencies(tests hyprcursor_test_c)
# Installation
install(DIRECTORY "include/hyprcursor" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} DIRECTORY_PERMISSIONS
OWNER_WRITE OWNER_READ OWNER_EXECUTE
GROUP_WRITE GROUP_READ GROUP_EXECUTE
WORLD_WRITE WORLD_READ WORLD_EXECUTE)
install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(TARGETS hyprcursor)
install(TARGETS hyprcursor-util)
install(DIRECTORY "include/hyprcursor" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
if(INSTALL_TESTS)
install(TARGETS hyprcursor_test1)
install(TARGETS hyprcursor_test2)
install(TARGETS hyprcursor_test_c)
endif()

View file

@ -16,6 +16,11 @@ doesn't suck as much.
- Support for SVG cursors
- Way more space-efficient. As an example, Bibata-XCursor is 44.1MB, while it's 6.6MB in hyprcursor.
## Documentation
See the [wiki here](https://wiki.hyprland.org/Hypr-Ecosystem/hyprcursor/)
check out [docs/](./docs)
and [standards](https://standards.hyprland.org/hyprcursor)
## Tools
### hyprcursor-util
@ -32,20 +37,6 @@ It provides C and C++ bindings.
For both C and C++, see `tests/`.
## Docs
See `docs/`.
## TODO
Library:
- [x] Support animated cursors
- [x] Support SVG cursors
Util:
- [ ] Support compiling a theme with X
- [x] Support decompiling animated cursors
## Building
### Deps:
@ -53,11 +44,12 @@ Util:
- cairo
- libzip
- librsvg
- tomlplusplus
### Build
```sh
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf _NPROCESSORS_CONF`
```
Install with:

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.12

View file

@ -49,13 +49,16 @@ Each cursor image is a separate directory. In it, multiple size variations can b
resize_algorithm = bilinear
# "hotspot" is where in your cursor the actual "click point" should be.
# this is in absolute coordinates. x+ is east, y+ is north.
# this is in absolute coordinates. x+ is east, y+ is south.
# the pixel coordinates of the hotspot at size are rounded to the nearest:
# (round(size * hotspot_x), round(size * hotspot_y))
hotspot_x = 0.0 # this goes 0 - 1
hotspot_y = 0.0 # this goes 0 - 1
# Define what cursor images this one should override.
# What this means is that a request for a cursor name e.g. "arrow"
# will instead use this one, even if this one is named something else.
# There is no unified list for all the available cursor names but this wayland list could be used as a reference https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/cursor-shape/cursor-shape-v1.xml#L71 for wayland specific cursors.
define_override = arrow
define_override = default
@ -69,6 +72,7 @@ define_size = 32, image32.png
# define_size = 64, anim2.png, 500
# define_size = 64, anim3.png, 500
# define_size = 64, anim4.png, 500
# Make sure the timeout is > 0, as otherwise the consumer might ignore your timeouts for being invalid.
```
Supported cursor image types are png and svg.
@ -77,4 +81,26 @@ If you are using an svg cursor, the size parameter will be ignored.
Mixing png and svg cursor images in one shape will result in an error.
Please note animated svgs are not supported, you need to add a separate svg for every frame.
All cursors are required to have an aspect ratio of 1:1.
Please note animated svgs are not supported, you need to add a separate svg for every frame.
### TOML
You are allowed to use TOML for all .hl files. Make sure to change the extension from `.hl` to `.toml`!
#### Manifest
Append `[General]` to the top, and wrap all the values in quotes.
#### Meta
Append `[General]` to the top, and wrap all values except hotspot in quotes.
Additionally, if you have multiple `define_*` keys, merge them into one like this:
```toml
define_override = 'shape1;shape2;shape3'
define_size = '24,image1.png,200;24,image2.png,200;32,image3.png,200'
```
You can put spaces around the semicolons if you prefer to.

View file

@ -2,17 +2,20 @@
"nodes": {
"hyprlang": {
"inputs": {
"hyprutils": "hyprutils",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1709914708,
"narHash": "sha256-bR4o3mynoTa1Wi4ZTjbnsZ6iqVcPGriXp56bZh5UFTk=",
"lastModified": 1737634606,
"narHash": "sha256-W7W87Cv6wqZ9PHegI6rH1+ve3zJPiyevMFf0/HwdbCQ=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "a685493fdbeec01ca8ccdf1f3655c044a8ce2fe2",
"rev": "f41271d35cc0f370d300413d756c2677f386af9d",
"type": "github"
},
"original": {
@ -21,13 +24,38 @@
"type": "github"
}
},
"hyprutils": {
"inputs": {
"nixpkgs": [
"hyprlang",
"nixpkgs"
],
"systems": [
"hyprlang",
"systems"
]
},
"locked": {
"lastModified": 1737632363,
"narHash": "sha256-X9I8POSlHxBVjD0fiX1O2j7U9Zi1+4rIkrsyHP0uHXY=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "006620eb29d54ea9086538891404c78563d1bae1",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprutils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1708475490,
"narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=",
"lastModified": 1737469691,
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0e74ca98a74bc7270d28838369593635a5db3260",
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"type": "github"
},
"original": {
@ -41,7 +69,7 @@
"inputs": {
"hyprlang": "hyprlang",
"nixpkgs": "nixpkgs",
"systems": "systems_2"
"systems": "systems"
}
},
"systems": {
@ -58,21 +86,6 @@
"repo": "default-linux",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
}
},
"root": "root",

View file

@ -7,6 +7,7 @@
hyprlang = {
url = "github:hyprwm/hyprlang";
inputs.systems.follows = "systems";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -29,7 +30,7 @@
packages = eachSystem (system: {
default = self.packages.${system}.hyprcursor;
inherit (pkgsFor.${system}) hyprcursor;
inherit (pkgsFor.${system}) hyprcursor hyprcursor-with-tests;
});
checks = eachSystem (system: self.packages.${system});

View file

@ -1,24 +0,0 @@
cmake_minimum_required(VERSION 3.19)
project(
hyprcursor-util
DESCRIPTION "A utility for creating and converting hyprcursor themes"
)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.0 libzip)
add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}")
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
set(CMAKE_CXX_STANDARD 23)
add_executable(hyprcursor-util ${SRCFILES})
target_link_libraries(hyprcursor-util PkgConfig::deps)
target_include_directories(hyprcursor-util
PRIVATE
.
)
install(TARGETS hyprcursor-util)

View file

@ -1 +0,0 @@
../libhyprcursor/internalSharedTypes.hpp

View file

@ -2,18 +2,26 @@
#include <zip.h>
#include <optional>
#include <filesystem>
#include <fstream>
#include <array>
#include <format>
#include <algorithm>
#include <regex>
#include <hyprlang.hpp>
#include "internalSharedTypes.hpp"
#include "manifest.hpp"
#include "meta.hpp"
enum eOperation {
#ifndef ZIP_LENGTH_TO_END
#define ZIP_LENGTH_TO_END -1
#endif
enum eOperation : uint8_t {
OPERATION_CREATE = 0,
OPERATION_EXTRACT = 1,
};
eResizeAlgo explicitResizeAlgo = RESIZE_INVALID;
static eHyprcursorResizeAlgo explicitResizeAlgo = HC_RESIZE_INVALID;
struct XCursorConfigEntry {
int size = 0, hotspotX = 0, hotspotY = 0, delay = 0;
@ -48,7 +56,7 @@ static bool promptForDeletion(const std::string& path) {
emptyDirectory = !std::count_if(std::filesystem::begin(IT), std::filesystem::end(IT), [](auto& e) { return e.is_regular_file(); });
}
if (!std::filesystem::exists(path + "/manifest.hl") && std::filesystem::exists(path) && !emptyDirectory) {
if (!std::filesystem::exists(path + "/manifest.hl") && !std::filesystem::exists(path + "/manifest.toml") && std::filesystem::exists(path) && !emptyDirectory) {
std::cout << "Refusing to remove " << path << " because it doesn't look like a hyprcursor theme.\n"
<< "Please set a valid, empty, nonexistent, or a theme directory as an output path\n";
exit(1);
@ -69,88 +77,25 @@ static bool promptForDeletion(const std::string& path) {
return true;
}
std::unique_ptr<SCursorTheme> currentTheme;
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
image.delay = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
image.filename = RHS;
try {
image.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
currentTheme->shapes.back()->images.push_back(image);
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
currentTheme->shapes.back()->overrides.push_back(V);
return result;
}
static std::optional<std::string> createCursorThemeFromPath(const std::string& path_, const std::string& out_ = {}) {
if (!std::filesystem::exists(path_))
return "input path does not exist";
SCursorTheme currentTheme;
const std::string path = std::filesystem::canonical(path_);
const auto MANIFESTPATH = path + "/manifest.hl";
if (!std::filesystem::exists(MANIFESTPATH))
return "manifest.hl is missing";
CManifest manifest(path + "/manifest");
const auto PARSERESULT = manifest.parse();
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->addConfigValue("description", Hyprlang::STRING{""});
manifest->addConfigValue("version", Hyprlang::STRING{""});
manifest->commence();
const auto RESULT = manifest->parse();
if (RESULT.error)
return "Manifest has errors: \n" + std::string{RESULT.getError()};
} catch (const char* err) { return "failed parsing manifest: " + std::string{err}; }
if (PARSERESULT.has_value())
return "couldn't parse manifest: " + *PARSERESULT;
const std::string THEMENAME = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
const std::string THEMENAME = manifest.parsedData.name;
std::string out = (out_.empty() ? path.substr(0, path.find_last_of('/') + 1) : out_) + "/theme_" + THEMENAME + "/";
std::string out = (out_.empty() ? path.substr(0, path.find_last_of('/')) : out_) + "/theme_" + THEMENAME;
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
const std::string CURSORSSUBDIR = manifest.parsedData.cursorsDirectory;
const std::string CURSORDIR = path + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
@ -158,28 +103,26 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
// iterate over the directory and record all cursors
currentTheme = std::make_unique<SCursorTheme>();
for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) {
const auto METAPATH = dir.path().string() + "/meta.hl";
if (!std::regex_match(dir.path().stem().string(), std::regex("^[A-Za-z0-9_\\-\\.]+$")))
return "Invalid cursor directory name at " + dir.path().string() + " : characters must be within [A-Za-z0-9_\\-\\.]";
auto& SHAPE = currentTheme->shapes.emplace_back(std::make_unique<SCursorShape>());
const auto METAPATH = dir.path().string() + "/meta";
auto& SHAPE = currentTheme.shapes.emplace_back(std::make_unique<SCursorShape>());
//
std::unique_ptr<Hyprlang::CConfig> meta;
CMeta meta{METAPATH, true, true};
const auto PARSERESULT2 = meta.parse();
try {
meta = std::make_unique<Hyprlang::CConfig>(METAPATH.c_str(), Hyprlang::SConfigOptions{});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
const auto RESULT = meta->parse();
if (PARSERESULT2.has_value())
return "couldn't parse meta: " + *PARSERESULT2;
if (RESULT.error)
return "meta.hl has errors: \n" + std::string{RESULT.getError()};
} catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; }
for (auto& i : meta.parsedData.definedSizes) {
SHAPE->images.push_back(SCursorImage{.filename = i.file, .size = i.size, .delay = i.delayMs});
}
SHAPE->overrides = meta.parsedData.overrides;
// check if we have at least one image.
for (auto& i : SHAPE->images) {
@ -209,9 +152,9 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
return "meta invalid: no images for shape " + dir.path().stem().string();
SHAPE->directory = dir.path().stem().string();
SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
SHAPE->hotspotX = meta.parsedData.hotspotX;
SHAPE->hotspotY = meta.parsedData.hotspotY;
SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo);
std::cout << "Shape " << SHAPE->directory << ": \n\toverrides: " << SHAPE->overrides.size() << "\n\tsizes: " << SHAPE->images.size() << "\n";
}
@ -226,13 +169,13 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
}
// manifest is copied
std::filesystem::copy(MANIFESTPATH, out + "/manifest.hl");
std::filesystem::copy(manifest.getPath(), out + "/manifest." + (manifest.getPath().ends_with(".hl") ? "hl" : "toml"));
// create subdir for cursors
std::filesystem::create_directory(out + "/" + CURSORSSUBDIR);
// create zips (.hlc) for each
for (auto& shape : currentTheme->shapes) {
for (auto& shape : currentTheme.shapes) {
const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape->directory;
const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape->directory + ".hlc";
int errp = 0;
@ -245,17 +188,18 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
}
// add meta.hl
zip_source_t* meta = zip_source_file(zip, (CURRENTCURSORSDIR + "/meta.hl").c_str(), 0, 0);
const auto METADIR = std::filesystem::exists(CURRENTCURSORSDIR + "/meta.hl") ? (CURRENTCURSORSDIR + "/meta.hl") : (CURRENTCURSORSDIR + "/meta.toml");
zip_source_t* meta = zip_source_file(zip, METADIR.c_str(), 0, ZIP_LENGTH_TO_END);
if (!meta)
return "(1) failed to add meta " + (CURRENTCURSORSDIR + "/meta.hl") + " to hlc";
if (zip_file_add(zip, "meta.hl", meta, ZIP_FL_ENC_UTF_8) < 0)
return "(2) failed to add meta " + (CURRENTCURSORSDIR + "/meta.hl") + " to hlc";
return "(1) failed to add meta " + METADIR + " to hlc";
if (zip_file_add(zip, (std::string{"meta."} + (METADIR.ends_with(".hl") ? "hl" : "toml")).c_str(), meta, ZIP_FL_ENC_UTF_8) < 0)
return "(2) failed to add meta " + METADIR + " to hlc";
meta = nullptr;
// add each cursor png
// add each cursor image
for (auto& i : shape->images) {
zip_source_t* image = zip_source_file(zip, (CURRENTCURSORSDIR + "/" + i.filename).c_str(), 0, 0);
zip_source_t* image = zip_source_file(zip, (CURRENTCURSORSDIR + "/" + i.filename).c_str(), 0, ZIP_LENGTH_TO_END);
if (!image)
return "(1) failed to add image " + (CURRENTCURSORSDIR + "/" + i.filename) + " to hlc";
if (zip_file_add(zip, (i.filename).c_str(), image, ZIP_FL_ENC_UTF_8) < 0)
@ -266,16 +210,15 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
// close zip and write
if (zip_close(zip) < 0) {
zip_error_t ziperror;
zip_error_init_with_code(&ziperror, errp);
return "Failed to write " + OUTPUTFILE + ": " + zip_error_strerror(&ziperror);
zip_error_t* ziperror = zip_get_error(zip);
return "Failed to write " + OUTPUTFILE + ": " + zip_error_strerror(ziperror);
}
std::cout << "Written " << OUTPUTFILE << "\n";
}
// done!
std::cout << "Done, written " << currentTheme->shapes.size() << " shapes.\n";
std::cout << "Done, written " << currentTheme.shapes.size() << " shapes.\n";
return {};
}
@ -342,7 +285,7 @@ static std::optional<std::string> extractXTheme(const std::string& xpath_, const
std::cout << "Found xcursor " << xcursor.path().stem().string() << "\n";
// decompile xcursor
const auto OUT = spawnSync(std::format("rm -f /tmp/hyprcursor-util/* && cd /tmp/hyprcursor-util && xcur2png {} -d /tmp/hyprcursor-util 2>&1",
const auto OUT = spawnSync(std::format("rm -f /tmp/hyprcursor-util/* && cd /tmp/hyprcursor-util && xcur2png '{}' -d /tmp/hyprcursor-util 2>&1",
std::filesystem::canonical(xcursor.path()).string()));
// read the config
@ -396,7 +339,7 @@ static std::optional<std::string> extractXTheme(const std::string& xpath_, const
}
// write a meta.hl
std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo));
std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == HC_RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo));
// find hotspot from first entry
metaString +=
@ -445,7 +388,7 @@ int main(int argc, char** argv, char** envp) {
eOperation op = OPERATION_CREATE;
std::string path = "", out = "";
for (size_t i = 1; i < argc; ++i) {
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "-v" || arg == "--version") {
@ -516,4 +459,4 @@ int main(int argc, char** argv, char** envp) {
}
return 0;
}
}

View file

@ -43,6 +43,13 @@ struct hyprcursor_cursor_style_info {
*/
CAPI struct hyprcursor_manager_t* hyprcursor_manager_create(const char* theme_name);
/*!
\since 0.1.6
Same as hyprcursor_manager_create, but with a logger.
*/
CAPI struct hyprcursor_manager_t* hyprcursor_manager_create_with_logger(const char* theme_name, PHYPRCURSORLOGFUNC fn);
/*!
Free a hyprcursor_manager_t*
*/
@ -71,7 +78,8 @@ CAPI int hyprcursor_load_theme_style(struct hyprcursor_manager_t* manager, struc
Once done with a size, call hyprcursor_style_done()
*/
CAPI hyprcursor_cursor_image_data** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info, int* out_size);
CAPI hyprcursor_cursor_image_data** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info,
int* out_size);
/*!
Free a returned hyprcursor_cursor_image_data.
@ -83,4 +91,32 @@ CAPI void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data,
*/
CAPI void hyprcursor_style_done(struct hyprcursor_manager_t* manager, struct hyprcursor_cursor_style_info info);
/*!
\since 0.1.6
Registers a logging function to a hyprcursor_manager_t*
PHYPRCURSORLOGFUNC's msg is owned by the caller and will be freed afterwards.
fn can be null to remove a logger.
*/
CAPI void hyprcursor_register_logging_function(struct hyprcursor_manager_t* manager, PHYPRCURSORLOGFUNC fn);
/*!
\since 0.1.6
Returns the raw image data of a cursor shape, not rendered at all, alongside the metadata.
The object needs to be freed instantly after using, see hyprcursor_raw_shape_data_free()
*/
CAPI hyprcursor_cursor_raw_shape_data* hyprcursor_get_raw_shape_data(struct hyprcursor_manager_t* manager, char* shape);
/*!
\since 0.1.6
See hyprcursor_get_raw_shape_data.
Frees the returned object.
*/
CAPI void hyprcursor_raw_shape_data_free(hyprcursor_cursor_raw_shape_data* data);
#endif

View file

@ -1,7 +1,8 @@
#pragma once
#include <vector>
#include <stdlib.h>
#include <cstdlib>
#include <string>
#include "shared.h"
@ -28,6 +29,40 @@ namespace Hyprcursor {
std::vector<SCursorImageData> images;
};
/*!
C++ structs for hyprcursor_cursor_raw_shape_image and hyprcursor_cursor_raw_shape_data
*/
struct SCursorRawShapeImage {
std::vector<unsigned char> data;
int size = 0;
int delay = 200;
};
struct SCursorRawShapeData {
std::vector<SCursorRawShapeImage> images;
float hotspotX = 0;
float hotspotY = 0;
std::string overridenBy = "";
eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NONE;
eHyprcursorDataType type = HC_DATA_PNG;
};
/*!
struct for cursor manager options
*/
struct SManagerOptions {
explicit SManagerOptions();
/*!
The function used for logging by the cursor manager
*/
PHYPRCURSORLOGFUNC logFn;
/*!
Allow fallback to env and first theme found
*/
bool allowDefaultFallback;
};
/*!
Basic Hyprcursor manager.
@ -39,10 +74,17 @@ namespace Hyprcursor {
If none found, bool valid() will be false.
If loading fails, bool valid() will be false.
If theme has no valid cursor shapes, bool valid() will be false.
*/
class CHyprcursorManager {
public:
CHyprcursorManager(const char* themeName);
/*!
\since 0.1.6
*/
CHyprcursorManager(const char* themeName, PHYPRCURSORLOGFUNC fn);
CHyprcursorManager(const char* themeName, SManagerOptions options);
~CHyprcursorManager();
/*!
@ -72,7 +114,7 @@ namespace Hyprcursor {
SCursorShapeData data;
for (size_t i = 0; i < size; ++i) {
for (int i = 0; i < size; ++i) {
SCursorImageData image;
image.delay = images[i]->delay;
image.size = images[i]->size;
@ -89,19 +131,70 @@ namespace Hyprcursor {
return data;
}
/*!
\since 0.1.6
Returns the raw image data of a cursor shape, not rendered at all, alongside the metadata.
*/
SCursorRawShapeData getRawShapeData(const char* shape_) {
auto CDATA = getRawShapeDataC(shape_);
if (CDATA->overridenBy) {
SCursorRawShapeData d{.overridenBy = CDATA->overridenBy};
free(CDATA->overridenBy);
delete CDATA;
return d;
}
SCursorRawShapeData data{.hotspotX = CDATA->hotspotX, .hotspotY = CDATA->hotspotY, .overridenBy = "", .resizeAlgo = CDATA->resizeAlgo, .type = CDATA->type};
for (size_t i = 0; i < CDATA->len; ++i) {
SCursorRawShapeImageC* cimage = &CDATA->images[i];
SCursorRawShapeImage& img = data.images.emplace_back();
img.size = cimage->size;
img.delay = cimage->delay;
img.data = std::vector<unsigned char>{(unsigned char*)cimage->data, (unsigned char*)cimage->data + (std::size_t)cimage->len};
}
delete[] CDATA->images;
delete CDATA;
return data;
}
/*!
Prefer getShape, this is for C compat.
*/
SCursorImageData** getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info);
/*!
Prefer getShapeData, this is for C compat.
*/
SCursorRawShapeDataC* getRawShapeDataC(const char* shape_);
/*!
Marks a certain style as done, allowing it to be potentially freed
*/
void cursorSurfaceStyleDone(const SCursorStyleInfo&);
/*!
\since 0.1.6
Registers a logging function to this manager.
PHYPRCURSORLOGFUNC's msg is owned by the caller and will be freed afterwards.
fn can be null to unregister a logger.
*/
void registerLoggingFunction(PHYPRCURSORLOGFUNC fn);
private:
CHyprcursorImplementation* impl = nullptr;
bool finalizedAndValid = false;
void init(const char* themeName_);
CHyprcursorImplementation* impl = nullptr;
bool finalizedAndValid = false;
bool allowDefaultFallback = true;
PHYPRCURSORLOGFUNC logFn = nullptr;
friend class CHyprcursorImplementation;
};
}
}

View file

@ -16,4 +16,52 @@ struct SCursorImageData {
typedef struct SCursorImageData hyprcursor_cursor_image_data;
enum eHyprcursorLogLevel {
HC_LOG_NONE = 0,
HC_LOG_TRACE,
HC_LOG_INFO,
HC_LOG_WARN,
HC_LOG_ERR,
HC_LOG_CRITICAL,
};
enum eHyprcursorDataType {
HC_DATA_PNG = 0,
HC_DATA_SVG,
};
enum eHyprcursorResizeAlgo {
HC_RESIZE_INVALID = 0,
HC_RESIZE_NONE,
HC_RESIZE_BILINEAR,
HC_RESIZE_NEAREST,
};
struct SCursorRawShapeImageC {
void* data;
unsigned long int len;
int size;
int delay;
};
typedef struct SCursorRawShapeImageC hyprcursor_cursor_raw_shape_image;
struct SCursorRawShapeDataC {
struct SCursorRawShapeImageC* images;
unsigned long int len;
float hotspotX;
float hotspotY;
char* overridenBy;
enum eHyprcursorResizeAlgo resizeAlgo;
enum eHyprcursorDataType type;
float nominalSize;
};
typedef struct SCursorRawShapeDataC hyprcursor_cursor_raw_shape_data;
/*
msg is owned by the caller and will be freed afterwards.
*/
typedef void (*PHYPRCURSORLOGFUNC)(enum eHyprcursorLogLevel level, char* msg);
#endif

View file

@ -1,53 +1,22 @@
#pragma once
enum eLogLevel {
TRACE = 0,
INFO,
LOG,
WARN,
ERR,
CRIT,
NONE
};
#include <string>
#include <format>
#include <iostream>
#include <hyprcursor/shared.h>
namespace Debug {
inline bool quiet = false;
inline bool verbose = false;
template <typename... Args>
void log(eLogLevel level, const std::string& fmt, Args&&... args) {
#ifndef HYPRLAND_DEBUG
// don't log in release
return;
#endif
if (!verbose && level == TRACE)
void log(eHyprcursorLogLevel level, PHYPRCURSORLOGFUNC fn, const std::string& fmt, Args&&... args) {
if (!fn)
return;
if (quiet)
return;
const std::string LOG = std::vformat(fmt, std::make_format_args(args...));
if (level != NONE) {
std::cout << '[';
switch (level) {
case TRACE: std::cout << "TRACE"; break;
case INFO: std::cout << "INFO"; break;
case LOG: std::cout << "LOG"; break;
case WARN: std::cout << "WARN"; break;
case ERR: std::cout << "ERR"; break;
case CRIT: std::cout << "CRITICAL"; break;
default: break;
}
std::cout << "] ";
}
std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n";
fn(level, (char*)LOG.c_str());
}
};

54
libhyprcursor/VarList.cpp Normal file
View file

@ -0,0 +1,54 @@
#include "VarList.hpp"
#include <ranges>
#include <algorithm>
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
CVarList::CVarList(const std::string& in, const size_t lastArgNo, const char delim, const bool removeEmpty) {
if (in.empty())
m_vArgs.emplace_back("");
std::string args{in};
size_t idx = 0;
size_t pos = 0;
std::ranges::replace_if(args, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0);
for (const auto& s : args | std::views::split(0)) {
if (removeEmpty && s.empty())
continue;
if (++idx == lastArgNo) {
m_vArgs.emplace_back(removeBeginEndSpacesTabs(in.substr(pos)));
break;
}
pos += s.size() + 1;
m_vArgs.emplace_back(removeBeginEndSpacesTabs(s.data()));
}
}
std::string CVarList::join(const std::string& joiner, size_t from, size_t to) const {
size_t last = to == 0 ? size() : to;
std::string rolling;
for (size_t i = from; i < last; ++i) {
rolling += m_vArgs[i] + (i + 1 < last ? joiner : "");
}
return rolling;
}

63
libhyprcursor/VarList.hpp Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include <functional>
#include <vector>
#include <string>
class CVarList {
public:
/** Split string into arg list
@param lastArgNo stop splitting after argv reaches maximum size, last arg will contain rest of unsplit args
@param delim if delimiter is 's', use std::isspace
@param removeEmpty remove empty args from argv
*/
CVarList(const std::string& in, const size_t maxSize = 0, const char delim = ',', const bool removeEmpty = false);
~CVarList() = default;
size_t size() const {
return m_vArgs.size();
}
std::string join(const std::string& joiner, size_t from = 0, size_t to = 0) const;
void map(std::function<void(std::string&)> func) {
for (auto& s : m_vArgs)
func(s);
}
void append(const std::string arg) {
m_vArgs.emplace_back(arg);
}
std::string operator[](const size_t& idx) const {
if (idx >= m_vArgs.size())
return "";
return m_vArgs[idx];
}
// for range-based loops
std::vector<std::string>::iterator begin() {
return m_vArgs.begin();
}
std::vector<std::string>::const_iterator begin() const {
return m_vArgs.begin();
}
std::vector<std::string>::iterator end() {
return m_vArgs.end();
}
std::vector<std::string>::const_iterator end() const {
return m_vArgs.end();
}
bool contains(const std::string& el) {
for (auto& a : m_vArgs) {
if (a == el)
return true;
}
return false;
}
private:
std::vector<std::string> m_vArgs;
};

View file

@ -2,33 +2,54 @@
#include "internalSharedTypes.hpp"
#include "internalDefines.hpp"
#include <array>
#include <cstddef>
#include <sstream>
#include <cstdio>
#include <filesystem>
#include <hyprlang.hpp>
#include <zip.h>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <librsvg/rsvg.h>
#include "manifest.hpp"
#include "meta.hpp"
#include "Log.hpp"
using namespace Hyprcursor;
// directories for lookup
constexpr const std::array<const char*, 1> systemThemeDirs = {"/usr/share/icons"};
static std::vector<std::string> getSystemThemeDirs() {
const auto envXdgData = std::getenv("XDG_DATA_DIRS");
std::vector<std::string> result;
if (envXdgData) {
std::stringstream envXdgStream(envXdgData);
std::string tmpStr;
while (getline(envXdgStream, tmpStr, ':'))
result.push_back((tmpStr + "/icons"));
} else
result = {"/usr/share/icons"};
return result;
}
const std::vector<std::string> systemThemeDirs = getSystemThemeDirs();
constexpr const std::array<const char*, 2> userThemeDirs = {"/.local/share/icons", "/.icons"};
//
static std::string themeNameFromEnv() {
static std::string themeNameFromEnv(PHYPRCURSORLOGFUNC logfn) {
const auto ENV = getenv("HYPRCURSOR_THEME");
if (!ENV)
if (!ENV) {
Debug::log(HC_LOG_INFO, logfn, "themeNameFromEnv: env unset");
return "";
}
return std::string{ENV};
}
static bool themeAccessible(const std::string& path) {
static bool pathAccessible(const std::string& path) {
try {
if (!std::filesystem::exists(path + "/manifest.hl"))
if (!std::filesystem::exists(path))
return false;
} catch (std::exception& e) { return false; }
@ -36,7 +57,11 @@ static bool themeAccessible(const std::string& path) {
return true;
}
static std::string getFirstTheme() {
static bool themeAccessible(const std::string& path) {
return pathAccessible(path + "/manifest.hl") || pathAccessible(path + "/manifest.toml");
}
static std::string getFirstTheme(PHYPRCURSORLOGFUNC logfn) {
// try user directories first
const auto HOMEENV = getenv("HOME");
@ -47,42 +72,60 @@ static std::string getFirstTheme() {
for (auto& dir : userThemeDirs) {
const auto FULLPATH = HOME + dir;
if (!std::filesystem::exists(FULLPATH))
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
if (std::filesystem::exists(MANIFESTPATH))
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.";
if (std::filesystem::exists(MANIFESTPATH + "hl") || std::filesystem::exists(MANIFESTPATH + "toml")) {
Debug::log(HC_LOG_INFO, logfn, "getFirstTheme: found {}", themeDir.path().string());
return themeDir.path().stem().string();
}
}
}
for (auto& dir : systemThemeDirs) {
const auto FULLPATH = dir;
if (!std::filesystem::exists(FULLPATH))
const auto& FULLPATH = dir;
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
if (std::filesystem::exists(MANIFESTPATH))
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.";
if (std::filesystem::exists(MANIFESTPATH + "hl") || std::filesystem::exists(MANIFESTPATH + "toml")) {
Debug::log(HC_LOG_INFO, logfn, "getFirstTheme: found {}", themeDir.path().string());
return themeDir.path().stem().string();
}
}
}
return "";
}
static std::string getFullPathForThemeName(const std::string& name) {
static std::string getFullPathForThemeName(const std::string& name, PHYPRCURSORLOGFUNC logfn, bool allowDefaultFallback) {
const auto HOMEENV = getenv("HOME");
if (!HOMEENV)
return "";
@ -91,85 +134,146 @@ static std::string getFullPathForThemeName(const std::string& name) {
for (auto& dir : userThemeDirs) {
const auto FULLPATH = HOME + dir;
if (!std::filesystem::exists(FULLPATH))
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
if (!name.empty() && themeDir.path().stem().string() != name)
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
const auto MANIFESTPATH = themeDir.path().string() + "/manifest";
if (allowDefaultFallback && name.empty()) {
if (std::filesystem::exists(MANIFESTPATH + ".hl") || std::filesystem::exists(MANIFESTPATH + ".toml")) {
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
}
continue;
}
CManifest manifest{MANIFESTPATH};
if (const auto R = manifest.parse(); R.has_value()) {
Debug::log(HC_LOG_ERR, logfn, "failed parsing Manifest of {}: {}", themeDir.path().string(), *R);
continue;
}
const std::string NAME = manifest.parsedData.name;
if (NAME != name && name != themeDir.path().stem().string())
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (std::filesystem::exists(MANIFESTPATH))
return std::filesystem::canonical(themeDir.path()).string();
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
}
}
for (auto& dir : systemThemeDirs) {
const auto FULLPATH = dir;
if (!std::filesystem::exists(FULLPATH))
const auto& FULLPATH = dir;
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
if (!name.empty() && themeDir.path().stem().string() != name)
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
const auto MANIFESTPATH = themeDir.path().string() + "/manifest";
CManifest manifest{MANIFESTPATH};
if (const auto R = manifest.parse(); R.has_value()) {
Debug::log(HC_LOG_ERR, logfn, "failed parsing Manifest of {}: {}", themeDir.path().string(), *R);
continue;
}
const std::string NAME = manifest.parsedData.name;
if (NAME != name && name != themeDir.path().stem().string())
continue;
if (!themeAccessible(themeDir.path().string()))
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (std::filesystem::exists(MANIFESTPATH))
return std::filesystem::canonical(themeDir.path()).string();
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
}
}
if (!name.empty()) // try without name
return getFullPathForThemeName("");
if (allowDefaultFallback && !name.empty()) { // try without name
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: failed, trying without name of {}", name);
return getFullPathForThemeName("", logfn, allowDefaultFallback);
}
return "";
}
SManagerOptions::SManagerOptions() : logFn(nullptr), allowDefaultFallback(true) {
;
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_) {
init(themeName_);
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_, PHYPRCURSORLOGFUNC fn) : logFn(fn) {
init(themeName_);
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_, SManagerOptions options) : allowDefaultFallback(options.allowDefaultFallback), logFn(options.logFn) {
init(themeName_);
}
void CHyprcursorManager::init(const char* themeName_) {
std::string themeName = themeName_ ? themeName_ : "";
if (themeName.empty()) {
if (allowDefaultFallback && themeName.empty()) {
// try reading from env
themeName = themeNameFromEnv();
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find theme from env");
themeName = themeNameFromEnv(logFn);
}
if (themeName.empty()) {
if (allowDefaultFallback && themeName.empty()) {
// try finding first, in the hierarchy
themeName = getFirstTheme();
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find any theme");
themeName = getFirstTheme(logFn);
}
if (themeName.empty()) {
// holy shit we're done
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: no themes matched");
return;
}
// initialize theme
impl = new CHyprcursorImplementation;
impl = new CHyprcursorImplementation(this, logFn);
impl->themeName = themeName;
impl->themeFullDir = getFullPathForThemeName(themeName);
impl->themeFullDir = getFullPathForThemeName(themeName, logFn, allowDefaultFallback);
if (impl->themeFullDir.empty())
return;
Debug::log(LOG, "Found theme {} at {}\n", impl->themeName, impl->themeFullDir);
Debug::log(HC_LOG_INFO, logFn, "Found theme {} at {}\n", impl->themeName, impl->themeFullDir);
const auto LOADSTATUS = impl->loadTheme();
if (LOADSTATUS.has_value()) {
Debug::log(ERR, "Theme failed to load with {}\n", LOADSTATUS.value());
Debug::log(HC_LOG_ERR, logFn, "Theme failed to load with {}\n", LOADSTATUS.value());
return;
}
if (impl->theme.shapes.empty()) {
Debug::log(HC_LOG_ERR, logFn, "Theme {} has no valid cursor shapes\n", impl->themeName);
return;
}
@ -177,8 +281,7 @@ CHyprcursorManager::CHyprcursorManager(const char* themeName_) {
}
CHyprcursorManager::~CHyprcursorManager() {
if (impl)
delete impl;
delete impl;
}
bool CHyprcursorManager::valid() {
@ -186,6 +289,11 @@ bool CHyprcursorManager::valid() {
}
SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info) {
if (!shape_) {
Debug::log(HC_LOG_ERR, logFn, "getShapesC: shape of nullptr is invalid");
return nullptr;
}
std::string REQUESTEDSHAPE = shape_;
std::vector<SLoadedCursorImage*> resultingImages;
@ -195,13 +303,15 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end())
continue;
const int PIXELSIDE = std::round(info.size / shape->nominalSize);
hotX = shape->hotspotX;
hotY = shape->hotspotY;
// matched :)
bool foundAny = false;
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side != info.size)
if (image->side != PIXELSIDE)
continue;
// found size
@ -213,22 +323,22 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
break;
// if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error.
if (shape->resizeAlgo != RESIZE_NONE) {
Debug::log(ERR, "getSurfaceFor didn't match a size?");
if (shape->resizeAlgo != HC_RESIZE_NONE) {
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match a size?");
return nullptr;
}
// find nearest
int leader = 13371337;
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (std::abs((int)(image->side - info.size)) > leader)
if (std::abs((int)(image->side - PIXELSIDE)) > std::abs((int)(leader - PIXELSIDE)))
continue;
leader = image->side;
}
if (leader == 13371337) { // ???
Debug::log(ERR, "getSurfaceFor didn't match any nearest size?");
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size?");
return nullptr;
}
@ -245,7 +355,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
if (foundAny)
break;
Debug::log(ERR, "getSurfaceFor didn't match any nearest size (2)?");
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size (2)?");
return nullptr;
}
@ -256,25 +366,94 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
data[i]->delay = resultingImages[i]->delay;
data[i]->size = resultingImages[i]->side;
data[i]->surface = resultingImages[i]->cairoSurface;
data[i]->hotspotX = hotX * data[i]->size;
data[i]->hotspotY = hotY * data[i]->size;
data[i]->hotspotX = std::round(hotX * (float)data[i]->size);
data[i]->hotspotY = std::round(hotY * (float)data[i]->size);
}
outSize = resultingImages.size();
Debug::log(HC_LOG_INFO, logFn, "getShapesC: found {} images for {}", outSize, shape_);
return data;
}
SCursorRawShapeDataC* CHyprcursorManager::getRawShapeDataC(const char* shape_) {
if (!shape_) {
Debug::log(HC_LOG_ERR, logFn, "getShapeDataC: shape of nullptr is invalid");
return nullptr;
}
const std::string SHAPE = shape_;
SCursorRawShapeDataC* data = new SCursorRawShapeDataC;
std::vector<SLoadedCursorImage*> resultingImages;
data->overridenBy = nullptr;
data->images = nullptr;
data->len = 0;
data->hotspotX = 0.f;
data->hotspotY = 0.F;
data->nominalSize = 1.F;
data->resizeAlgo = eHyprcursorResizeAlgo::HC_RESIZE_NONE;
data->type = eHyprcursorDataType::HC_DATA_PNG;
for (auto& shape : impl->theme.shapes) {
// if it's overridden just return the override
if (const auto IT = std::find_if(shape->overrides.begin(), shape->overrides.end(), [&](const auto& e) { return e == SHAPE && SHAPE != shape->directory; });
IT != shape->overrides.end()) {
data->overridenBy = strdup(shape->directory.c_str());
return data;
}
}
for (auto& shape : impl->theme.shapes) {
if (shape->directory != SHAPE)
continue;
if (!impl->loadedShapes.contains(shape.get()))
continue; // ??
// found it
for (auto& i : impl->loadedShapes[shape.get()].images) {
resultingImages.push_back(i.get());
}
data->hotspotX = shape->hotspotX;
data->hotspotY = shape->hotspotY;
data->nominalSize = shape->nominalSize;
data->type = shape->shapeType == SHAPE_PNG ? HC_DATA_PNG : HC_DATA_SVG;
break;
}
data->len = resultingImages.size();
data->images = new SCursorRawShapeImageC[data->len];
for (size_t i = 0; i < data->len; ++i) {
data->images[i].data = resultingImages[i]->data;
data->images[i].len = resultingImages[i]->dataLen;
data->images[i].size = resultingImages[i]->side;
data->images[i].delay = resultingImages[i]->delay;
}
return data;
}
bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
Debug::log(HC_LOG_INFO, logFn, "loadThemeStyle: loading for size {}", info.size);
for (auto& shape : impl->theme.shapes) {
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG)
continue; // don't resample NONE style cursors
if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG) {
// don't resample NONE style cursors
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: ignoring {}", shape->directory);
continue;
}
bool sizeFound = false;
if (shape->shapeType == SHAPE_PNG) {
const int IDEALSIDE = std::round(info.size / shape->nominalSize);
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side != info.size)
if (image->side != IDEALSIDE)
continue;
sizeFound = true;
@ -288,7 +467,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
SLoadedCursorImage* leader = nullptr;
int leaderVal = 1000000;
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side < info.size)
if (image->side < IDEALSIDE)
continue;
if (image->side > leaderVal)
@ -300,7 +479,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
if (!leader) {
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (std::abs((int)(image->side - info.size)) > leaderVal)
if (std::abs((int)(image->side - IDEALSIDE)) > leaderVal)
continue;
leaderVal = image->side;
@ -309,23 +488,29 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
}
if (!leader) {
Debug::log(ERR, "Resampling failed to find a candidate???");
Debug::log(HC_LOG_ERR, logFn, "Resampling failed to find a candidate???");
return false;
}
const auto FRAMES = impl->getFramesFor(shape.get(), leader->side);
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape {} has {} frames", shape->directory, FRAMES.size());
const int PIXELSIDE = std::round(leader->side / shape->nominalSize);
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape has nominal {:.2f}, pixel size will be {}x", shape->nominalSize, PIXELSIDE);
for (auto& f : FRAMES) {
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true;
newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
newImage->side = PIXELSIDE;
newImage->artificialData = new char[static_cast<unsigned long>(PIXELSIDE * PIXELSIDE * 4)];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, PIXELSIDE, PIXELSIDE, PIXELSIDE * 4);
newImage->delay = f->delay;
const auto PCAIRO = cairo_create(newImage->cairoSurface);
cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE);
cairo_set_antialias(PCAIRO, shape->resizeAlgo == HC_RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE);
cairo_save(PCAIRO);
cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR);
@ -334,12 +519,12 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
const auto PTN = cairo_pattern_create_for_surface(f->cairoSurface);
cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE);
const float scale = info.size / (float)f->side;
const float scale = PIXELSIDE / (float)f->side;
cairo_scale(PCAIRO, scale, scale);
cairo_pattern_set_filter(PTN, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
cairo_pattern_set_filter(PTN, shape->resizeAlgo == HC_RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
cairo_set_source(PCAIRO, PTN);
cairo_rectangle(PCAIRO, 0, 0, info.size, info.size);
cairo_rectangle(PCAIRO, 0, 0, PIXELSIDE, PIXELSIDE);
cairo_fill(PCAIRO);
cairo_surface_flush(newImage->cairoSurface);
@ -350,12 +535,18 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
} else if (shape->shapeType == SHAPE_SVG) {
const auto FRAMES = impl->getFramesFor(shape.get(), 0);
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape {} has {} frames", shape->directory, FRAMES.size());
const int PIXELSIDE = std::round(info.size / shape->nominalSize);
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape has nominal {:.2f}, pixel size will be {}x", shape->nominalSize, PIXELSIDE);
for (auto& f : FRAMES) {
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true;
newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
newImage->side = PIXELSIDE;
newImage->artificialData = new char[static_cast<unsigned long>(PIXELSIDE * PIXELSIDE * 4)];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, PIXELSIDE, PIXELSIDE, PIXELSIDE * 4);
newImage->delay = f->delay;
const auto PCAIRO = cairo_create(newImage->cairoSurface);
@ -369,23 +560,25 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
RsvgHandle* handle = rsvg_handle_new_from_data((unsigned char*)f->data, f->dataLen, &error);
if (!handle) {
Debug::log(ERR, "Failed reading svg: {}", error->message);
Debug::log(HC_LOG_ERR, logFn, "Failed reading svg: {}", error->message);
return false;
}
RsvgRectangle rect = {0, 0, (double)info.size, (double)info.size};
RsvgRectangle rect = {0, 0, (double)PIXELSIDE, (double)PIXELSIDE};
if (!rsvg_handle_render_document(handle, PCAIRO, &rect, &error)) {
Debug::log(ERR, "Failed rendering svg: {}", error->message);
Debug::log(HC_LOG_ERR, logFn, "Failed rendering svg: {}", error->message);
g_object_unref(handle);
return false;
}
// done
cairo_surface_flush(newImage->cairoSurface);
cairo_destroy(PCAIRO);
g_object_unref(handle);
}
} else {
Debug::log(ERR, "Invalid shapetype in loadThemeStyle");
Debug::log(HC_LOG_ERR, logFn, "Invalid shapetype in loadThemeStyle");
return false;
}
}
@ -395,7 +588,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
for (auto& shape : impl->theme.shapes) {
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG)
if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG)
continue;
std::erase_if(impl->loadedShapes[shape.get()].images, [info, &shape](const auto& e) {
@ -403,7 +596,7 @@ void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
const bool isArtificial = e->artificial;
// clean artificial rasters made for this
if (isArtificial && e->side == info.size)
if (isArtificial && e->side == std::round(info.size / shape->nominalSize))
return true;
// clean invalid non-svg rasters
@ -415,83 +608,8 @@ void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
}
}
/*
Implementation
*/
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
SCursorTheme* currentTheme;
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
image.delay = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
image.filename = RHS;
try {
image.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
currentTheme->shapes.back()->images.push_back(image);
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
currentTheme->shapes.back()->overrides.push_back(V);
return result;
void CHyprcursorManager::registerLoggingFunction(PHYPRCURSORLOGFUNC fn) {
logFn = fn;
}
/*
@ -503,20 +621,17 @@ PNG reading
static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int len) {
const auto DATA = (SLoadedCursorImage*)data;
if (DATA->readNeedle >= DATA->dataLen)
return CAIRO_STATUS_READ_ERROR;
if (!DATA->data)
return CAIRO_STATUS_READ_ERROR;
size_t toRead = len > DATA->dataLen - DATA->readNeedle ? DATA->dataLen - DATA->readNeedle : len;
std::memcpy(output, DATA->data + DATA->readNeedle, toRead);
std::memcpy(output, (uint8_t*)DATA->data + DATA->readNeedle, toRead);
DATA->readNeedle += toRead;
if (DATA->readNeedle >= DATA->dataLen) {
delete[] (char*)DATA->data;
DATA->data = nullptr;
Debug::log(TRACE, "cairo: png read, freeing mem");
}
return CAIRO_STATUS_SUCCESS;
}
@ -531,30 +646,24 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
if (!themeAccessible(themeFullDir))
return "Theme inaccessible";
currentTheme = &theme;
// load manifest
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) {
Debug::log(ERR, "Failed parsing manifest due to {}", err);
return std::string{"failed: "} + err;
}
CManifest manifest(themeFullDir + "/manifest");
const auto PARSERESULT = manifest.parse();
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
if (PARSERESULT.has_value())
return "couldn't parse manifest: " + *PARSERESULT;
const std::string CURSORSSUBDIR = manifest.parsedData.cursorsDirectory;
const std::string CURSORDIR = themeFullDir + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
return "loadTheme: cursors_directory missing or empty";
for (auto& cursor : std::filesystem::directory_iterator(CURSORDIR)) {
if (!cursor.is_regular_file())
if (!cursor.is_regular_file()) {
Debug::log(HC_LOG_TRACE, logFn, "loadTheme: skipping {}", cursor.path().string());
continue;
}
auto& SHAPE = theme.shapes.emplace_back(std::make_unique<SCursorShape>());
auto& LOADEDSHAPE = loadedShapes[SHAPE.get()];
@ -563,13 +672,22 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
int errp = 0;
zip_t* zip = zip_open(cursor.path().string().c_str(), ZIP_RDONLY, &errp);
zip_file_t* meta_file = zip_fopen(zip, "meta.hl", ZIP_FL_UNCHANGED);
if (!meta_file)
zip_int64_t index = zip_name_locate(zip, "meta.hl", ZIP_FL_ENC_GUESS);
bool metaIsHL = true;
if (index == -1) {
index = zip_name_locate(zip, "meta.toml", ZIP_FL_ENC_GUESS);
metaIsHL = false;
}
if (index == -1)
return "cursor" + cursor.path().string() + "failed to load meta";
char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */
zip_file_t* meta_file = zip_fopen_index(zip, index, ZIP_FL_UNCHANGED);
int readBytes = zip_fread(meta_file, buffer, 1024 * 1024 - 1);
char* buffer = new char[static_cast<unsigned long>(1024 * 1024)]; /* 1MB should be more than enough */
int readBytes = zip_fread(meta_file, buffer, (1024 * 1024) - 1);
zip_fclose(meta_file);
@ -580,21 +698,23 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
buffer[readBytes] = '\0';
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(buffer, Hyprlang::SConfigOptions{.pathIsStream = true});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
meta->parse();
} catch (const char* err) { return "failed parsing meta: " + std::string{err}; }
CMeta meta{buffer, metaIsHL};
delete[] buffer;
const auto METAPARSERESULT = meta.parse();
if (METAPARSERESULT.has_value())
return "cursor" + cursor.path().string() + "failed to parse meta: " + *METAPARSERESULT;
for (auto& i : meta.parsedData.definedSizes) {
SHAPE->images.push_back(SCursorImage{.filename = i.file, .size = i.size, .delay = i.delayMs});
}
SHAPE->overrides = meta.parsedData.overrides;
zip_stat_t sb;
zip_stat_init(&sb);
for (auto& i : SHAPE->images) {
if (SHAPE->shapeType == SHAPE_INVALID) {
if (i.filename.ends_with(".svg"))
@ -602,7 +722,7 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
else if (i.filename.ends_with(".png"))
SHAPE->shapeType = SHAPE_PNG;
else {
std::cout << "WARNING: image " << i.filename << " has no known extension, assuming png.\n";
Debug::log(HC_LOG_WARN, logFn, "WARNING: image {} has no known extension, assuming png.", i.filename);
SHAPE->shapeType = SHAPE_PNG;
}
} else {
@ -613,24 +733,33 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
}
// load image
Debug::log(TRACE, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
Debug::log(HC_LOG_TRACE, logFn, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get();
IMAGE->side = i.size;
IMAGE->side = SHAPE->shapeType == SHAPE_SVG ? 0 : i.size;
IMAGE->delay = i.delay;
IMAGE->isSVG = SHAPE->shapeType == SHAPE_SVG;
// read from zip
zip_file_t* image_file = zip_fopen(zip, i.filename.c_str(), ZIP_FL_UNCHANGED);
if (!image_file)
index = zip_name_locate(zip, i.filename.c_str(), ZIP_FL_ENC_GUESS);
if (index == -1)
return "cursor" + cursor.path().string() + "failed to load image_file";
IMAGE->data = new char[1024 * 1024]; /* 1MB should be more than enough, again. This probably should be in the spec. */
// read from zip
zip_file_t* image_file = zip_fopen_index(zip, index, ZIP_FL_UNCHANGED);
zip_stat_index(zip, index, ZIP_FL_UNCHANGED, &sb);
IMAGE->dataLen = zip_fread(image_file, IMAGE->data, 1024 * 1024 - 1);
if (sb.valid & ZIP_STAT_SIZE) {
IMAGE->data = new char[sb.size + 1];
IMAGE->dataLen = sb.size + 1;
} else {
IMAGE->data = new char[static_cast<unsigned long>(1024 * 1024)]; /* 1MB should be more than enough, again. This probably should be in the spec. */
IMAGE->dataLen = static_cast<size_t>(1024 * 1024);
}
IMAGE->dataLen = zip_fread(image_file, IMAGE->data, IMAGE->dataLen - 1);
zip_fclose(image_file);
Debug::log(TRACE, "Cairo: set up surface read");
Debug::log(HC_LOG_TRACE, logFn, "Cairo: set up surface read");
if (SHAPE->shapeType == SHAPE_PNG) {
@ -642,17 +771,18 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
return "Failed reading cairoSurface, status " + std::to_string((int)STATUS);
}
} else {
Debug::log(LOG, "Skipping cairo load for a svg surface");
Debug::log(HC_LOG_TRACE, logFn, "Skipping cairo load for a svg surface");
}
}
if (SHAPE->images.empty())
return "meta invalid: no images for shape " + cursor.path().stem().string();
SHAPE->directory = cursor.path().stem().string();
SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
SHAPE->directory = cursor.path().stem().string();
SHAPE->hotspotX = meta.parsedData.hotspotX;
SHAPE->hotspotY = meta.parsedData.hotspotY;
SHAPE->nominalSize = meta.parsedData.nominalSize;
SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo);
zip_discard(zip);
}
@ -674,4 +804,4 @@ std::vector<SLoadedCursorImage*> CHyprcursorImplementation::getFramesFor(SCursor
}
return frames;
}
}

View file

@ -7,6 +7,10 @@ hyprcursor_manager_t* hyprcursor_manager_create(const char* theme_name) {
return (hyprcursor_manager_t*)new CHyprcursorManager(theme_name);
}
hyprcursor_manager_t* hyprcursor_manager_create_with_logger(const char* theme_name, PHYPRCURSORLOGFUNC fn) {
return (hyprcursor_manager_t*)new CHyprcursorManager(theme_name, fn);
}
void hyprcursor_manager_free(hyprcursor_manager_t* manager) {
delete (CHyprcursorManager*)manager;
}
@ -34,7 +38,7 @@ struct SCursorImageData** hyprcursor_get_cursor_image_data(struct hyprcursor_man
}
void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data, int size) {
for (size_t i = 0; i < size; ++i) {
for (int i = 0; i < size; ++i) {
free(data[i]);
}
@ -45,5 +49,26 @@ void hyprcursor_style_done(hyprcursor_manager_t* manager, hyprcursor_cursor_styl
const auto MGR = (CHyprcursorManager*)manager;
SCursorStyleInfo info;
info.size = info_.size;
return MGR->cursorSurfaceStyleDone(info);
}
MGR->cursorSurfaceStyleDone(info);
}
void hyprcursor_register_logging_function(struct hyprcursor_manager_t* manager, PHYPRCURSORLOGFUNC fn) {
const auto MGR = (CHyprcursorManager*)manager;
MGR->registerLoggingFunction(fn);
}
CAPI hyprcursor_cursor_raw_shape_data* hyprcursor_get_raw_shape_data(struct hyprcursor_manager_t* manager, char* shape) {
const auto MGR = (CHyprcursorManager*)manager;
return MGR->getRawShapeDataC(shape);
}
CAPI void hyprcursor_raw_shape_data_free(hyprcursor_cursor_raw_shape_data* data) {
if (data->overridenBy) {
free(data->overridenBy);
delete data;
return;
}
delete[] data->images;
delete data;
}

View file

@ -18,7 +18,7 @@ struct SLoadedCursorImage {
// read stuff
size_t readNeedle = 0;
void* data = nullptr;
void* data = nullptr; // raw png / svg data, not image data
size_t dataLen = 0;
bool isSVG = false; // if true, data is just a string of chars
@ -37,10 +37,17 @@ struct SLoadedCursorShape {
class CHyprcursorImplementation {
public:
std::string themeName;
std::string themeFullDir;
CHyprcursorImplementation(Hyprcursor::CHyprcursorManager* mgr, PHYPRCURSORLOGFUNC fn) : owner(mgr), logFn(fn) {
;
}
SCursorTheme theme;
Hyprcursor::CHyprcursorManager* owner = nullptr;
PHYPRCURSORLOGFUNC logFn = nullptr;
std::string themeName;
std::string themeFullDir;
SCursorTheme theme;
//
std::unordered_map<SCursorShape*, SLoadedCursorShape> loadedShapes;

View file

@ -2,13 +2,7 @@
#include <string>
#include <vector>
#include <memory>
enum eResizeAlgo {
RESIZE_INVALID = 0,
RESIZE_NONE,
RESIZE_BILINEAR,
RESIZE_NEAREST,
};
#include <hyprcursor/shared.h>
enum eShapeType {
SHAPE_INVALID = 0,
@ -16,19 +10,19 @@ enum eShapeType {
SHAPE_SVG,
};
inline eResizeAlgo stringToAlgo(const std::string& s) {
inline eHyprcursorResizeAlgo stringToAlgo(const std::string& s) {
if (s == "none")
return RESIZE_NONE;
return HC_RESIZE_NONE;
if (s == "nearest")
return RESIZE_NEAREST;
return RESIZE_BILINEAR;
return HC_RESIZE_NEAREST;
return HC_RESIZE_BILINEAR;
}
inline std::string algoToString(const eResizeAlgo a) {
inline std::string algoToString(const eHyprcursorResizeAlgo a) {
switch (a) {
case RESIZE_BILINEAR: return "bilinear";
case RESIZE_NEAREST: return "nearest";
case RESIZE_NONE: return "none";
case HC_RESIZE_BILINEAR: return "bilinear";
case HC_RESIZE_NEAREST: return "nearest";
case HC_RESIZE_NONE: return "none";
default: return "none";
}
@ -43,8 +37,8 @@ struct SCursorImage {
struct SCursorShape {
std::string directory;
float hotspotX = 0, hotspotY = 0;
eResizeAlgo resizeAlgo = RESIZE_NEAREST;
float hotspotX = 0, hotspotY = 0, nominalSize = 1.F;
eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NEAREST;
std::vector<SCursorImage> images;
std::vector<std::string> overrides;
eShapeType shapeType = SHAPE_INVALID;

View file

@ -0,0 +1,75 @@
#include "manifest.hpp"
#include <toml++/toml.hpp>
#include <hyprlang.hpp>
#include <filesystem>
CManifest::CManifest(const std::string& path_) {
try {
if (std::filesystem::exists(path_ + ".hl")) {
path = path_ + ".hl";
selectedParser = PARSER_HYPRLANG;
return;
}
if (std::filesystem::exists(path_ + ".toml")) {
path = path_ + ".toml";
selectedParser = PARSER_TOML;
return;
}
} catch (...) { ; }
}
std::optional<std::string> CManifest::parse() {
if (path.empty())
return "Failed to find an appropriate manifest.";
if (selectedParser == PARSER_HYPRLANG)
return parseHL();
if (selectedParser == PARSER_TOML)
return parseTOML();
return "No parser available for " + path;
}
std::optional<std::string> CManifest::parseHL() {
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>(path.c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->addConfigValue("description", Hyprlang::STRING{""});
manifest->addConfigValue("version", Hyprlang::STRING{""});
manifest->addConfigValue("author", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) { return std::string{"failed: "} + err; }
parsedData.cursorsDirectory = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
parsedData.name = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
parsedData.description = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("description"));
parsedData.version = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("version"));
parsedData.author = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("author"));
return {};
}
std::optional<std::string> CManifest::parseTOML() {
try {
auto MANIFEST = toml::parse_file(path);
parsedData.cursorsDirectory = MANIFEST["General"]["cursors_directory"].value_or("");
parsedData.name = MANIFEST["General"]["name"].value_or("");
parsedData.description = MANIFEST["General"]["description"].value_or("");
parsedData.version = MANIFEST["General"]["version"].value_or("");
parsedData.author = MANIFEST["General"]["author"].value_or("");
} catch (...) { return "Failed parsing toml"; }
return {};
}
std::string CManifest::getPath() {
return path;
}

View file

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <optional>
/*
Manifest can parse manifest.hl and manifest.toml
*/
class CManifest {
public:
/*
path_ is the path to a manifest WITHOUT the extension.
CManifest will attempt all parsable extensions (.hl, .toml)
*/
CManifest(const std::string& path_);
std::optional<std::string> parse();
std::string getPath();
struct {
std::string name, description, version, cursorsDirectory, author;
} parsedData;
private:
enum eParser {
PARSER_HYPRLANG = 0,
PARSER_TOML
};
std::optional<std::string> parseHL();
std::optional<std::string> parseTOML();
eParser selectedParser = PARSER_HYPRLANG;
std::string path;
};

194
libhyprcursor/meta.cpp Normal file
View file

@ -0,0 +1,194 @@
#include "meta.hpp"
#include <hyprlang.hpp>
#include <toml++/toml.hpp>
#include <filesystem>
#include <regex>
#include <algorithm>
#include "VarList.hpp"
static CMeta* currentMeta = nullptr;
CMeta::CMeta(const std::string& rawdata_, bool hyprlang_ /* false for toml */, bool dataIsPath) : dataPath(dataIsPath), hyprlang(hyprlang_), rawdata(rawdata_) {
if (!dataIsPath)
return;
rawdata = "";
try {
if (std::filesystem::exists(rawdata_ + ".hl")) {
rawdata = rawdata_ + ".hl";
hyprlang = true;
return;
}
if (std::filesystem::exists(rawdata_ + ".toml")) {
rawdata = rawdata_ + ".toml";
hyprlang = false;
return;
}
} catch (...) {}
}
std::optional<std::string> CMeta::parse() {
if (rawdata.empty())
return "Invalid meta (missing?)";
std::optional<std::string> res;
currentMeta = this;
if (hyprlang)
res = parseHL();
else
res = parseTOML();
currentMeta = nullptr;
return res;
}
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
CVarList sizes(VALUE, 0, ';');
for (const auto& sizeStr : sizes) {
if (!sizeStr.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(sizeStr.substr(0, sizeStr.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(sizeStr.substr(sizeStr.find_first_of(",") + 1));
auto DELAY = 0;
CMeta::SDefinedSize size;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(',')));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(',') + 1));
try {
size.delayMs = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
if (!std::regex_match(RHS, std::regex("^[A-Za-z0-9_\\-\\.]+$"))) {
result.setError("Invalid cursor file name, characters must be within [A-Za-z0-9_\\-\\.] (if this seems like a mistake, check for invisible characters)");
return result;
}
size.file = RHS;
if (!size.file.ends_with(".svg")) {
try {
size.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
} else
size.size = 0;
currentMeta->parsedData.definedSizes.push_back(size);
}
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
CVarList overrides(VALUE, 0, ';');
for (const auto& o : overrides) {
currentMeta->parsedData.overrides.push_back(o);
}
return result;
}
std::optional<std::string> CMeta::parseHL() {
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(rawdata.c_str(), Hyprlang::SConfigOptions{.pathIsStream = !dataPath});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("nominal_size", Hyprlang::FLOAT{1.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
const auto RESULT = meta->parse();
if (RESULT.error)
return RESULT.getError();
} catch (const char* err) { return "failed parsing meta: " + std::string{err}; }
parsedData.hotspotX = std::any_cast<Hyprlang::FLOAT>(meta->getConfigValue("hotspot_x"));
parsedData.hotspotY = std::any_cast<Hyprlang::FLOAT>(meta->getConfigValue("hotspot_y"));
parsedData.nominalSize = std::clamp(std::any_cast<Hyprlang::FLOAT>(meta->getConfigValue("nominal_size")), 0.1F, 2.F);
parsedData.resizeAlgo = std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm"));
return {};
}
std::optional<std::string> CMeta::parseTOML() {
try {
auto MANIFEST = dataPath ? toml::parse_file(rawdata) : toml::parse(rawdata);
parsedData.hotspotX = MANIFEST["General"]["hotspot_x"].value_or(0.f);
parsedData.hotspotY = MANIFEST["General"]["hotspot_y"].value_or(0.f);
parsedData.nominalSize = std::clamp(MANIFEST["General"]["nominal_size"].value_or(1.F), 0.1F, 2.F);
const std::string OVERRIDES = MANIFEST["General"]["define_override"].value_or("");
const std::string SIZES = MANIFEST["General"]["define_size"].value_or("");
//
CVarList OVERRIDESLIST(OVERRIDES, 0, ';', true);
for (auto& o : OVERRIDESLIST) {
const auto RESULT = ::parseOverride("define_override", o.c_str());
if (RESULT.error)
throw;
}
CVarList SIZESLIST(SIZES, 0, ';', true);
for (auto& s : SIZESLIST) {
const auto RESULT = ::parseDefineSize("define_size", s.c_str());
if (RESULT.error)
throw;
}
parsedData.resizeAlgo = MANIFEST["General"]["resize_algorithm"].value_or("");
} catch (std::exception& e) { return std::string{"Failed parsing toml: "} + e.what(); }
return {};
}

36
libhyprcursor/meta.hpp Normal file
View file

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <optional>
#include <vector>
/*
Meta can parse meta.hl and meta.toml
*/
class CMeta {
public:
CMeta(const std::string& rawdata_, bool hyprlang_ /* false for toml */, bool dataIsPath = false);
std::optional<std::string> parse();
struct SDefinedSize {
std::string file;
int size = 0, delayMs = 200;
};
struct {
std::string resizeAlgo;
float hotspotX = 0, hotspotY = 0, nominalSize = 1.F;
std::vector<std::string> overrides;
std::vector<SDefinedSize> definedSizes;
} parsedData;
private:
std::optional<std::string> parseHL();
std::optional<std::string> parseTOML();
bool dataPath = false;
bool hyprlang = true;
std::string rawdata;
};

View file

@ -7,6 +7,8 @@
hyprlang,
librsvg,
libzip,
xcur2png,
tomlplusplus,
version ? "git",
}:
stdenv.mkDerivation {
@ -24,6 +26,8 @@ stdenv.mkDerivation {
hyprlang
librsvg
libzip
xcur2png
tomlplusplus
];
outputs = [

View file

@ -7,6 +7,7 @@
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
version = lib.removeSuffix "\n" (builtins.readFile ../VERSION);
in {
default = inputs.self.overlays.hyprcursor;
@ -14,10 +15,14 @@ in {
inputs.hyprlang.overlays.default
(final: prev: {
hyprcursor = prev.callPackage ./default.nix {
stdenv = prev.gcc13Stdenv;
version = "0.pre" + "+date=" + (mkDate (inputs.self.lastModifiedDate or "19700101")) + "_" + (inputs.self.shortRev or "dirty");
stdenv = prev.gcc14Stdenv;
version = version + "+date=" + (mkDate (inputs.self.lastModifiedDate or "19700101")) + "_" + (inputs.self.shortRev or "dirty");
inherit (final) hyprlang;
};
hyprcursor-with-tests = final.hyprcursor.overrideAttrs (_: _: {
cmakeFlags = [(lib.cmakeBool "INSTALL_TESTS" true)];
});
})
];
}

79
tests/c_test.c Normal file
View file

@ -0,0 +1,79 @@
/*
hyprlang-test in C.
Renders a cursor shape to /tmp at 48px
For better explanations, see the cpp tests.
*/
#include <hyprcursor/hyprcursor.h>
#include <stdio.h>
#include <stdlib.h>
void logFunction(enum eHyprcursorLogLevel level, char* message) {
printf("[hc] %s\n", message);
}
int main(int argc, char** argv) {
struct hyprcursor_manager_t* mgr = hyprcursor_manager_create_with_logger(NULL, logFunction);
if (!mgr) {
printf("mgr null\n");
return 1;
}
if (!hyprcursor_manager_valid(mgr)) {
printf("mgr is invalid\n");
return 1;
}
hyprcursor_cursor_raw_shape_data* shapeData = hyprcursor_get_raw_shape_data(mgr, "left_ptr");
if (!shapeData) {
printf("failed querying left_ptr\n");
return 1;
}
if (shapeData->overridenBy) {
hyprcursor_cursor_raw_shape_data* ov = hyprcursor_get_raw_shape_data(mgr, shapeData->overridenBy);
hyprcursor_raw_shape_data_free(shapeData);
shapeData = ov;
}
if (!shapeData || shapeData->len <= 0) {
printf("left_ptr has no images\n");
return 1;
}
printf("left_ptr images: %ld\n", shapeData->len);
for (size_t i = 0; i < shapeData->len; ++i) {
printf("left_ptr image size: %ld\n", shapeData->images[i].len);
}
hyprcursor_raw_shape_data_free(shapeData);
shapeData = NULL;
struct hyprcursor_cursor_style_info info = {.size = 48};
if (!hyprcursor_load_theme_style(mgr, info)) {
printf("load failed\n");
return 1;
}
int dataSize = 0;
hyprcursor_cursor_image_data** data = hyprcursor_get_cursor_image_data(mgr, "left_ptr", info, &dataSize);
if (data == NULL) {
printf("data failed\n");
return 1;
}
int ret = cairo_surface_write_to_png(data[0]->surface, "/tmp/arrowC.png");
hyprcursor_cursor_image_data_free(data, dataSize);
hyprcursor_style_done(mgr, info);
if (ret) {
printf("cairo failed\n");
return 1;
}
return 0;
}

76
tests/full_rendering.cpp Normal file
View file

@ -0,0 +1,76 @@
/*
full_rendering.cpp
This example shows probably what you want to do.
Hyprcursor will render a left_ptr shape at 48x48px to a file called /tmp/arrow.png
*/
#include <iostream>
#include <hyprcursor/hyprcursor.hpp>
void logFunction(enum eHyprcursorLogLevel level, char* message) {
std::cout << "[hc] " << message << "\n";
}
int main(int argc, char** argv) {
/*
Create a manager. You can optionally pass a logger function.
*/
Hyprcursor::CHyprcursorManager mgr(nullptr, logFunction);
/*
Manager could be invalid if no themes were found, or
a specified theme was invalid.
*/
if (!mgr.valid()) {
std::cout << "mgr is invalid\n";
return 1;
}
/*
Style describes what pixel size you want your cursor
images to be.
Remember to free styles once you're done with them
(e.g. the user requested to change the cursor size to something else)
*/
Hyprcursor::SCursorStyleInfo style{.size = 48};
if (!mgr.loadThemeStyle(style)) {
std::cout << "failed loading style\n";
return 1;
}
/*
Get a shape. This will return the data about available image(s),
their delay, hotspot, etc.
*/
const auto SHAPEDATA = mgr.getShape("left_ptr", style);
/*
If the size doesn't exist, images will be empty.
*/
if (SHAPEDATA.images.empty()) {
std::cout << "no images\n";
return 1;
}
std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n";
/*
Save to disk with cairo
*/
const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png");
std::cout << "Cairo returned for write: " << RET << "\n";
/*
As mentioned before, clean up by releasing the style.
*/
mgr.cursorSurfaceStyleDone(style);
if (RET)
return 1;
return !mgr.valid();
}

80
tests/only_metadata.cpp Normal file
View file

@ -0,0 +1,80 @@
/*
only_metadata.cpp
This is a mode in which you probably do NOT want to operate,
but major DEs might want their own renderer for
cursor shapes.
Prefer full_rendering.cpp for consistency and simplicity.
*/
#include <iostream>
#include <hyprcursor/hyprcursor.hpp>
void logFunction(enum eHyprcursorLogLevel level, char* message) {
std::cout << "[hc] " << message << "\n";
}
int main(int argc, char** argv) {
/*
Create a manager. You can optionally pass a logger function.
*/
Hyprcursor::CHyprcursorManager mgr(nullptr, logFunction);
/*
Manager could be invalid if no themes were found, or
a specified theme was invalid.
*/
if (!mgr.valid()) {
std::cout << "mgr is invalid\n";
return 1;
}
/*
If you are planning on using your own renderer,
you do not want to load in any styles, as those
are rendered once you make your call.
Instead, let's request left_ptr's metadata
*/
auto RAWDATA = mgr.getRawShapeData("left_ptr");
/*
if images are empty, check overridenBy
*/
if (RAWDATA.images.empty()) {
/*
if overridenBy is empty, the current theme doesn't have this shape.
*/
if (RAWDATA.overridenBy.empty())
return false;
/*
load what it's overriden by.
*/
RAWDATA = mgr.getRawShapeData(RAWDATA.overridenBy.c_str());
}
/*
If we still have no images, the theme seems broken.
*/
if (RAWDATA.images.empty()) {
std::cout << "failed querying left_ptr\n";
return 1;
}
/*
You can query the images (animation frames)
or their properties.
Every image has .data and .type for you to handle.
*/
std::cout << "left_ptr images: " << RAWDATA.images.size() << "\n";
for (auto& i : RAWDATA.images)
std::cout << "left_ptr data size: " << i.data.size() << "\n";
return 0;
}

View file

@ -1,37 +0,0 @@
#include <hyprcursor/hyprcursor.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv) {
struct hyprcursor_manager_t* mgr = hyprcursor_manager_create(NULL);
if (!mgr) {
printf("mgr null\n");
return 1;
}
struct hyprcursor_cursor_style_info info = {.size = 48};
if (!hyprcursor_load_theme_style(mgr, info)) {
printf("load failed\n");
return 1;
}
int dataSize = 0;
hyprcursor_cursor_image_data** data = hyprcursor_get_cursor_image_data(mgr, "left_ptr", info, &dataSize);
if (data == NULL) {
printf("data failed\n");
return 1;
}
int ret = cairo_surface_write_to_png(data[0]->surface, "/tmp/arrowC.png");
hyprcursor_cursor_image_data_free(data, dataSize);
hyprcursor_style_done(mgr, info);
if (ret) {
printf("cairo failed\n");
return 1;
}
return 0;
}

View file

@ -1,36 +0,0 @@
#include <iostream>
#include <hyprcursor/hyprcursor.hpp>
int main(int argc, char** argv) {
Hyprcursor::CHyprcursorManager mgr(nullptr);
Hyprcursor::SCursorStyleInfo style{.size = 48};
// preload size 48 for testing
if (!mgr.loadThemeStyle(style)) {
std::cout << "failed loading style\n";
return 1;
}
// get cursor for left_ptr
const auto SHAPEDATA = mgr.getShape("left_ptr", style);
if (SHAPEDATA.images.empty()) {
std::cout << "no images\n";
return 1;
}
std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n";
// save to disk
const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png");
std::cout << "Cairo returned for write: " << RET << "\n";
mgr.cursorSurfaceStyleDone(style);
if (RET)
return 1;
return !mgr.valid();
}