mirror of
https://github.com/hyprwm/hyprcursor.git
synced 2025-05-13 05:40:38 +01:00
Compare commits
86 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ac903e80b3 | ||
![]() |
2fd36421c2 | ||
![]() |
028bedbc63 | ||
![]() |
7c6d165e1e | ||
![]() |
0a8e83d35b | ||
![]() |
43e5139076 | ||
![]() |
dcadd3398a | ||
![]() |
9c5dd1f7c8 | ||
![]() |
3219b31128 | ||
![]() |
69270ba8f0 | ||
![]() |
3b3259e52a | ||
![]() |
abc1c60eb5 | ||
![]() |
84203d8126 | ||
![]() |
f388aacd22 | ||
![]() |
c18572a92e | ||
![]() |
0264e69814 | ||
![]() |
70fb494aa6 | ||
![]() |
572cb49bb7 | ||
![]() |
53a23e4b41 | ||
![]() |
d60e1e01e6 | ||
![]() |
5729b9733d | ||
![]() |
34efe230c2 | ||
![]() |
704cd7fed0 | ||
![]() |
e8acfdb903 | ||
![]() |
6b4131ee52 | ||
![]() |
66648429bd | ||
![]() |
b98726e431 | ||
![]() |
912d56025f | ||
![]() |
5a95d8512b | ||
![]() |
4493a972b4 | ||
![]() |
04cb4df80a | ||
![]() |
4efcbd36a2 | ||
![]() |
a5c0d57325 | ||
![]() |
66d5b46ff9 | ||
![]() |
dd3a853c82 | ||
![]() |
c50b2f0f1d | ||
![]() |
d5f4a6c708 | ||
![]() |
9e27a2c2ce | ||
![]() |
57298fc4f1 | ||
![]() |
27ca640abe | ||
![]() |
7c3aa03dff | ||
![]() |
dfba774650 | ||
![]() |
4a32d0cf25 | ||
![]() |
cab4746180 | ||
![]() |
0a53b9957f | ||
![]() |
c38dcf160d | ||
![]() |
1f4c960cf1 | ||
![]() |
92af141a01 | ||
![]() |
d41e8ac8d1 | ||
![]() |
178717746d | ||
![]() |
f6a6322a03 | ||
![]() |
6742e9d3e2 | ||
![]() |
e5e3d140a4 | ||
![]() |
17ebb7fff0 | ||
![]() |
bd56398f19 | ||
![]() |
af4ce3953d | ||
![]() |
65507c093f | ||
![]() |
033416cedc | ||
![]() |
95cd9376e8 | ||
![]() |
818d8c4b69 | ||
![]() |
7561459770 | ||
![]() |
981b661782 | ||
![]() |
08fbf37b1c | ||
![]() |
7266c021cd | ||
![]() |
6b1dc5e15a | ||
![]() |
d780013ffa | ||
![]() |
aaccfdc83d | ||
![]() |
4781252877 | ||
![]() |
f4ea0297a0 | ||
![]() |
be7e9f93cf | ||
![]() |
752cc44779 | ||
![]() |
73721de9ae | ||
![]() |
d3876f3477 | ||
![]() |
1a1fcfb58d | ||
![]() |
44d46e45a1 | ||
![]() |
75751ed957 | ||
![]() |
22a4195557 | ||
![]() |
f870f0f980 | ||
![]() |
7bd0d55aa2 | ||
![]() |
6a92473237 | ||
![]() |
8a874fc49c | ||
![]() |
e3694ecf2f | ||
![]() |
4b9efbed7a | ||
![]() |
59acebef20 | ||
![]() |
60f9c53cf2 | ||
![]() |
1761f6cefd |
34 changed files with 1643 additions and 563 deletions
101
.clang-tidy
Normal file
101
.clang-tidy
Normal 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
|
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
|
@ -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
39
.github/workflows/test.yml
vendored
Normal 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
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.vscode/
|
||||
build/
|
||||
.cache/
|
||||
|
|
133
CMakeLists.txt
133
CMakeLists.txt
|
@ -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()
|
||||
|
|
22
README.md
22
README.md
|
@ -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
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
0.1.12
|
|
@ -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.
|
||||
|
|
59
flake.lock
59
flake.lock
|
@ -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",
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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)
|
|
@ -1 +0,0 @@
|
|||
../libhyprcursor/internalSharedTypes.hpp
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
54
libhyprcursor/VarList.cpp
Normal 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
63
libhyprcursor/VarList.hpp
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
75
libhyprcursor/manifest.cpp
Normal file
75
libhyprcursor/manifest.cpp
Normal 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;
|
||||
}
|
36
libhyprcursor/manifest.hpp
Normal file
36
libhyprcursor/manifest.hpp
Normal 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
194
libhyprcursor/meta.cpp
Normal 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
36
libhyprcursor/meta.hpp
Normal 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;
|
||||
};
|
|
@ -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 = [
|
||||
|
|
|
@ -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
79
tests/c_test.c
Normal 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
76
tests/full_rendering.cpp
Normal 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
80
tests/only_metadata.cpp
Normal 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;
|
||||
}
|
37
tests/test.c
37
tests/test.c
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
Loading…
Reference in a new issue