From f6be58e58ea44e27c24172842f3301d1fa991008 Mon Sep 17 00:00:00 2001 From: "alan (NyxTrail)" Date: Sun, 14 Apr 2024 17:55:27 +0000 Subject: [PATCH] New upstream version 0.1.7 --- .github/workflows/ci.yaml | 2 +- CMakeLists.txt | 31 +- docs/MAKING_THEMES.md | 26 +- flake.nix | 1 + hyprcursor-util/CMakeLists.txt | 24 -- hyprcursor-util/internalSharedTypes.hpp | 1 - hyprcursor-util/src/main.cpp | 133 ++------ include/hyprcursor/hyprcursor.h | 38 ++- include/hyprcursor/hyprcursor.hpp | 73 +++++ include/hyprcursor/shared.h | 47 +++ libhyprcursor/Log.hpp | 43 +-- libhyprcursor/VarList.cpp | 55 ++++ libhyprcursor/VarList.hpp | 63 ++++ libhyprcursor/hyprcursor.cpp | 416 +++++++++++++----------- libhyprcursor/hyprcursor_c.cpp | 25 ++ libhyprcursor/internalDefines.hpp | 15 +- libhyprcursor/internalSharedTypes.hpp | 26 +- libhyprcursor/manifest.cpp | 75 +++++ libhyprcursor/manifest.hpp | 36 ++ libhyprcursor/meta.cpp | 174 ++++++++++ libhyprcursor/meta.hpp | 36 ++ nix/default.nix | 7 + nix/dirs.patch | 13 + tests/{test.c => c_test.c} | 29 +- tests/full_rendering.cpp | 76 +++++ tests/only_metadata.cpp | 80 +++++ tests/test.cpp | 40 --- 27 files changed, 1160 insertions(+), 425 deletions(-) delete mode 100644 hyprcursor-util/CMakeLists.txt delete mode 120000 hyprcursor-util/internalSharedTypes.hpp create mode 100644 libhyprcursor/VarList.cpp create mode 100644 libhyprcursor/VarList.hpp create mode 100644 libhyprcursor/manifest.cpp create mode 100644 libhyprcursor/manifest.hpp create mode 100644 libhyprcursor/meta.cpp create mode 100644 libhyprcursor/meta.hpp create mode 100644 nix/dirs.patch rename tests/{test.c => c_test.c} (58%) create mode 100644 tests/full_rendering.cpp create mode 100644 tests/only_metadata.cpp delete mode 100644 tests/test.cpp diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05a5431..52b6153 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ 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: Install hyprlang run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index a5362c4..6a3b7bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.19) -set(HYPRCURSOR_VERSION "0.1.5") +set(HYPRCURSOR_VERSION "0.1.7") add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}") project(hyprcursor @@ -20,7 +20,7 @@ configure_file(hyprcursor.pc.in hyprcursor.pc @ONLY) set(CMAKE_CXX_STANDARD 23) find_package(PkgConfig REQUIRED) -pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.2 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") @@ -54,23 +54,34 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 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_dependencies(tests hyprcursor_test_c) # Installation +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) diff --git a/docs/MAKING_THEMES.md b/docs/MAKING_THEMES.md index c0c04fd..56ab550 100644 --- a/docs/MAKING_THEMES.md +++ b/docs/MAKING_THEMES.md @@ -50,6 +50,8 @@ 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. +# 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 @@ -77,4 +79,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. \ No newline at end of file +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. \ No newline at end of file diff --git a/flake.nix b/flake.nix index bcee089..535cfee 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,7 @@ hyprlang = { url = "github:hyprwm/hyprlang"; + inputs.systems.follows = "systems"; inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/hyprcursor-util/CMakeLists.txt b/hyprcursor-util/CMakeLists.txt deleted file mode 100644 index 81d2847..0000000 --- a/hyprcursor-util/CMakeLists.txt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/hyprcursor-util/internalSharedTypes.hpp b/hyprcursor-util/internalSharedTypes.hpp deleted file mode 120000 index 9402209..0000000 --- a/hyprcursor-util/internalSharedTypes.hpp +++ /dev/null @@ -1 +0,0 @@ -../libhyprcursor/internalSharedTypes.hpp \ No newline at end of file diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp index f219380..8602d6b 100644 --- a/hyprcursor-util/src/main.cpp +++ b/hyprcursor-util/src/main.cpp @@ -7,13 +7,15 @@ #include #include #include "internalSharedTypes.hpp" +#include "manifest.hpp" +#include "meta.hpp" enum eOperation { OPERATION_CREATE = 0, OPERATION_EXTRACT = 1, }; -eResizeAlgo explicitResizeAlgo = RESIZE_INVALID; +eHyprcursorResizeAlgo explicitResizeAlgo = HC_RESIZE_INVALID; struct XCursorConfigEntry { int size = 0, hotspotX = 0, hotspotY = 0, delay = 0; @@ -48,7 +50,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 +71,25 @@ static bool promptForDeletion(const std::string& path) { return true; } -std::unique_ptr 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 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 manifest; - try { - manifest = std::make_unique(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(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 + "/"; - const std::string CURSORSSUBDIR = std::any_cast(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 +97,21 @@ static std::optional createCursorThemeFromPath(const std::string& p // iterate over the directory and record all cursors - currentTheme = std::make_unique(); for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) { - const auto METAPATH = dir.path().string() + "/meta.hl"; + const auto METAPATH = dir.path().string() + "/meta"; - auto& SHAPE = currentTheme->shapes.emplace_back(std::make_unique()); + auto& SHAPE = currentTheme.shapes.emplace_back(std::make_unique()); // - std::unique_ptr meta; + CMeta meta{METAPATH, true, true}; + const auto PARSERESULT2 = meta.parse(); - try { - meta = std::make_unique(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{i.file, i.size, i.delayMs}); + } // check if we have at least one image. for (auto& i : SHAPE->images) { @@ -209,9 +141,9 @@ static std::optional 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(meta->getConfigValue("hotspot_x")); - SHAPE->hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); - SHAPE->resizeAlgo = stringToAlgo(std::any_cast(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 +158,13 @@ static std::optional 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,11 +177,12 @@ static std::optional 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, 0); 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; @@ -275,7 +208,7 @@ static std::optional createCursorThemeFromPath(const std::string& p } // done! - std::cout << "Done, written " << currentTheme->shapes.size() << " shapes.\n"; + std::cout << "Done, written " << currentTheme.shapes.size() << " shapes.\n"; return {}; } @@ -396,7 +329,7 @@ static std::optional 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 += diff --git a/include/hyprcursor/hyprcursor.h b/include/hyprcursor/hyprcursor.h index 7110375..b82ea45 100644 --- a/include/hyprcursor/hyprcursor.h +++ b/include/hyprcursor/hyprcursor.h @@ -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 \ No newline at end of file diff --git a/include/hyprcursor/hyprcursor.hpp b/include/hyprcursor/hyprcursor.hpp index 7e8fc99..0e0f280 100644 --- a/include/hyprcursor/hyprcursor.hpp +++ b/include/hyprcursor/hyprcursor.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "shared.h" @@ -28,6 +29,24 @@ namespace Hyprcursor { std::vector images; }; + /*! + C++ structs for hyprcursor_cursor_raw_shape_image and hyprcursor_cursor_raw_shape_data + */ + struct SCursorRawShapeImage { + std::vector data; + int size = 0; + int delay = 200; + }; + + struct SCursorRawShapeData { + std::vector images; + float hotspotX = 0; + float hotspotY = 0; + std::string overridenBy = ""; + eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NONE; + eHyprcursorDataType type = HC_DATA_PNG; + }; + /*! Basic Hyprcursor manager. @@ -43,6 +62,10 @@ namespace Hyprcursor { class CHyprcursorManager { public: CHyprcursorManager(const char* themeName); + /*! + \since 0.1.6 + */ + CHyprcursorManager(const char* themeName, PHYPRCURSORLOGFUNC fn); ~CHyprcursorManager(); /*! @@ -89,19 +112,69 @@ 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*)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: + void init(const char* themeName_); + CHyprcursorImplementation* impl = nullptr; bool finalizedAndValid = false; + PHYPRCURSORLOGFUNC logFn = nullptr; + + friend class CHyprcursorImplementation; }; } \ No newline at end of file diff --git a/include/hyprcursor/shared.h b/include/hyprcursor/shared.h index d4416ad..c3fb9c6 100644 --- a/include/hyprcursor/shared.h +++ b/include/hyprcursor/shared.h @@ -16,4 +16,51 @@ 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; +}; + +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 diff --git a/libhyprcursor/Log.hpp b/libhyprcursor/Log.hpp index 75ad948..361c82c 100644 --- a/libhyprcursor/Log.hpp +++ b/libhyprcursor/Log.hpp @@ -1,53 +1,22 @@ #pragma once -enum eLogLevel { - TRACE = 0, - INFO, - LOG, - WARN, - ERR, - CRIT, - NONE -}; - #include #include #include +#include + namespace Debug { inline bool quiet = false; inline bool verbose = false; template - 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()); } }; \ No newline at end of file diff --git a/libhyprcursor/VarList.cpp b/libhyprcursor/VarList.cpp new file mode 100644 index 0000000..518acde --- /dev/null +++ b/libhyprcursor/VarList.cpp @@ -0,0 +1,55 @@ +#include "VarList.hpp" +#include +#include + +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(std::string_view{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; +} \ No newline at end of file diff --git a/libhyprcursor/VarList.hpp b/libhyprcursor/VarList.hpp new file mode 100644 index 0000000..1374da6 --- /dev/null +++ b/libhyprcursor/VarList.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include + +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 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::iterator begin() { + return m_vArgs.begin(); + } + std::vector::const_iterator begin() const { + return m_vArgs.begin(); + } + std::vector::iterator end() { + return m_vArgs.end(); + } + std::vector::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 m_vArgs; +}; \ No newline at end of file diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp index d0f8a22..a78d2ba 100644 --- a/libhyprcursor/hyprcursor.cpp +++ b/libhyprcursor/hyprcursor.cpp @@ -3,12 +3,14 @@ #include "internalDefines.hpp" #include #include -#include #include #include #include +#include #include +#include "manifest.hpp" +#include "meta.hpp" #include "Log.hpp" using namespace Hyprcursor; @@ -18,17 +20,19 @@ constexpr const std::array systemThemeDirs = {"/usr/share/icons" constexpr const std::array 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 +40,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 +55,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)) + 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) { const auto HOMEENV = getenv("HOME"); if (!HOMEENV) return ""; @@ -91,103 +117,134 @@ 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; - const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl"; - - if (name.empty()) { - if (std::filesystem::exists(MANIFESTPATH)) - return std::filesystem::canonical(themeDir.path()).string(); + 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)) - continue; - - std::unique_ptr manifest; - try { - manifest = std::make_unique(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{}); - manifest->addConfigValue("name", Hyprlang::STRING{""}); - manifest->commence(); - manifest->parse(); - } catch (const char* e) { continue; } - - const std::string NAME = std::any_cast(manifest->getConfigValue("name")); - - if (NAME != name) + const auto MANIFESTPATH = themeDir.path().string() + "/manifest"; + + if (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; + 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)) + 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 (!name.empty()) { // try without name + Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: failed, trying without name of {}", name); + return getFullPathForThemeName("", logfn); + } return ""; } CHyprcursorManager::CHyprcursorManager(const char* themeName_) { + init(themeName_); +} + +CHyprcursorManager::CHyprcursorManager(const char* themeName_, PHYPRCURSORLOGFUNC fn) { + logFn = fn; + init(themeName_); +} + +void CHyprcursorManager::init(const char* themeName_) { std::string themeName = themeName_ ? themeName_ : ""; if (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()) { // 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); 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; } @@ -204,6 +261,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 resultingImages; @@ -231,8 +293,8 @@ 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; } @@ -246,7 +308,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap } 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; } @@ -263,7 +325,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; } @@ -274,19 +336,74 @@ 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 resultingImages; + + for (auto& shape : impl->theme.shapes) { + // if it's overridden just return the override + if (const auto IT = std::find(shape->overrides.begin(), shape->overrides.end(), SHAPE); IT != shape->overrides.end()) { + data->overridenBy = strdup(IT->c_str()); + return data; + } + + 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->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; @@ -327,12 +444,14 @@ 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()); + for (auto& f : FRAMES) { auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); newImage->artificial = true; @@ -343,7 +462,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { 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); @@ -354,7 +473,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); const float scale = info.size / (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); @@ -368,6 +487,8 @@ 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()); + for (auto& f : FRAMES) { auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); newImage->artificial = true; @@ -387,14 +508,14 @@ 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}; 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); return false; } @@ -403,7 +524,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { cairo_destroy(PCAIRO); } } else { - Debug::log(ERR, "Invalid shapetype in loadThemeStyle"); + Debug::log(HC_LOG_ERR, logFn, "Invalid shapetype in loadThemeStyle"); return false; } } @@ -413,7 +534,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) { @@ -433,86 +554,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; - - if (!image.filename.ends_with(".svg")) { - try { - image.size = std::stoull(LHS); - } catch (std::exception& e) { - result.setError(e.what()); - return result; - } - } else - image.size = 0; - - 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; } /* @@ -524,6 +567,9 @@ 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; @@ -532,12 +578,6 @@ static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int le 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; } @@ -552,30 +592,24 @@ std::optional CHyprcursorImplementation::loadTheme() { if (!themeAccessible(themeFullDir)) return "Theme inaccessible"; - currentTheme = &theme; - // load manifest - std::unique_ptr manifest; - try { - // TODO: unify this between util and lib - manifest = std::make_unique((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(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()); auto& LOADEDSHAPE = loadedShapes[SHAPE.get()]; @@ -585,8 +619,13 @@ std::optional CHyprcursorImplementation::loadTheme() { 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) - return "cursor" + cursor.path().string() + "failed to load meta"; + bool metaIsHL = true; + if (!meta_file) { + meta_file = zip_fopen(zip, "meta.toml", ZIP_FL_UNCHANGED); + metaIsHL = false; + if (!meta_file) + return "cursor" + cursor.path().string() + "failed to load meta"; + } char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */ @@ -601,21 +640,20 @@ std::optional CHyprcursorImplementation::loadTheme() { buffer[readBytes] = '\0'; - std::unique_ptr meta; - - try { - meta = std::make_unique(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{i.file, i.size, i.delayMs}); + } + + SHAPE->overrides = meta.parsedData.overrides; + for (auto& i : SHAPE->images) { if (SHAPE->shapeType == SHAPE_INVALID) { if (i.filename.ends_with(".svg")) @@ -623,7 +661,7 @@ std::optional 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 { @@ -634,7 +672,7 @@ std::optional 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()).get(); IMAGE->side = SHAPE->shapeType == SHAPE_SVG ? 0 : i.size; IMAGE->delay = i.delay; @@ -651,19 +689,19 @@ std::optional CHyprcursorImplementation::loadTheme() { 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) { IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE); if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) { - delete[] (char*)IMAGE->data; + delete[](char*) IMAGE->data; IMAGE->data = nullptr; 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"); } } @@ -671,9 +709,9 @@ std::optional CHyprcursorImplementation::loadTheme() { return "meta invalid: no images for shape " + cursor.path().stem().string(); SHAPE->directory = cursor.path().stem().string(); - SHAPE->hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); - SHAPE->hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); - SHAPE->resizeAlgo = stringToAlgo(std::any_cast(meta->getConfigValue("resize_algorithm"))); + SHAPE->hotspotX = meta.parsedData.hotspotX; + SHAPE->hotspotY = meta.parsedData.hotspotY; + SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo); zip_discard(zip); } @@ -695,4 +733,4 @@ std::vector CHyprcursorImplementation::getFramesFor(SCursor } return frames; -} \ No newline at end of file +} diff --git a/libhyprcursor/hyprcursor_c.cpp b/libhyprcursor/hyprcursor_c.cpp index 2277bb0..764031d 100644 --- a/libhyprcursor/hyprcursor_c.cpp +++ b/libhyprcursor/hyprcursor_c.cpp @@ -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; } @@ -46,4 +50,25 @@ void hyprcursor_style_done(hyprcursor_manager_t* manager, hyprcursor_cursor_styl SCursorStyleInfo info; info.size = info_.size; return 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; } \ No newline at end of file diff --git a/libhyprcursor/internalDefines.hpp b/libhyprcursor/internalDefines.hpp index f6c1c14..ef087e2 100644 --- a/libhyprcursor/internalDefines.hpp +++ b/libhyprcursor/internalDefines.hpp @@ -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 loadedShapes; diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp index c5ac601..3a032aa 100644 --- a/libhyprcursor/internalSharedTypes.hpp +++ b/libhyprcursor/internalSharedTypes.hpp @@ -2,13 +2,7 @@ #include #include #include - -enum eResizeAlgo { - RESIZE_INVALID = 0, - RESIZE_NONE, - RESIZE_BILINEAR, - RESIZE_NEAREST, -}; +#include 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"; } @@ -44,7 +38,7 @@ struct SCursorImage { struct SCursorShape { std::string directory; float hotspotX = 0, hotspotY = 0; - eResizeAlgo resizeAlgo = RESIZE_NEAREST; + eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NEAREST; std::vector images; std::vector overrides; eShapeType shapeType = SHAPE_INVALID; diff --git a/libhyprcursor/manifest.cpp b/libhyprcursor/manifest.cpp new file mode 100644 index 0000000..1118f50 --- /dev/null +++ b/libhyprcursor/manifest.cpp @@ -0,0 +1,75 @@ +#include "manifest.hpp" + +#include +#include + +#include + +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 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 CManifest::parseHL() { + std::unique_ptr manifest; + try { + // TODO: unify this between util and lib + manifest = std::make_unique(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(manifest->getConfigValue("cursors_directory")); + parsedData.name = std::any_cast(manifest->getConfigValue("name")); + parsedData.description = std::any_cast(manifest->getConfigValue("description")); + parsedData.version = std::any_cast(manifest->getConfigValue("version")); + parsedData.author = std::any_cast(manifest->getConfigValue("author")); + + return {}; +} + +std::optional 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; +} \ No newline at end of file diff --git a/libhyprcursor/manifest.hpp b/libhyprcursor/manifest.hpp new file mode 100644 index 0000000..d5cc170 --- /dev/null +++ b/libhyprcursor/manifest.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +/* + 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 parse(); + std::string getPath(); + + struct { + std::string name, description, version, cursorsDirectory, author; + } parsedData; + + private: + enum eParser { + PARSER_HYPRLANG = 0, + PARSER_TOML + }; + + std::optional parseHL(); + std::optional parseTOML(); + + eParser selectedParser = PARSER_HYPRLANG; + + std::string path; +}; \ No newline at end of file diff --git a/libhyprcursor/meta.cpp b/libhyprcursor/meta.cpp new file mode 100644 index 0000000..77e554e --- /dev/null +++ b/libhyprcursor/meta.cpp @@ -0,0 +1,174 @@ +#include "meta.hpp" + +#include +#include +#include + +#include "VarList.hpp" + +CMeta* currentMeta = nullptr; + +CMeta::CMeta(const std::string& rawdata_, bool hyprlang_ /* false for toml */, bool dataIsPath) : rawdata(rawdata_), hyprlang(hyprlang_), dataPath(dataIsPath) { + 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 CMeta::parse() { + if (rawdata.empty()) + return "Invalid meta (missing?)"; + + std::optional 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; + + 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; + + 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; + } + + 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; + + currentMeta->parsedData.overrides.push_back(VALUE); + + return result; +} + +std::optional CMeta::parseHL() { + std::unique_ptr meta; + + try { + meta = std::make_unique(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("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}; } + + parsedData.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + parsedData.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + parsedData.resizeAlgo = std::any_cast(meta->getConfigValue("resize_algorithm")); + + return {}; +} + +std::optional 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); + + 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 {}; +} diff --git a/libhyprcursor/meta.hpp b/libhyprcursor/meta.hpp new file mode 100644 index 0000000..837d5ee --- /dev/null +++ b/libhyprcursor/meta.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +/* + 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 parse(); + + struct SDefinedSize { + std::string file; + int size = 0, delayMs = 200; + }; + + struct { + std::string resizeAlgo; + float hotspotX = 0, hotspotY = 0; + std::vector overrides; + std::vector definedSizes; + } parsedData; + + private: + std::optional parseHL(); + std::optional parseTOML(); + + bool dataPath = false; + bool hyprlang = true; + + std::string rawdata; +}; \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix index e989bd9..281af4b 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -7,6 +7,7 @@ hyprlang, librsvg, libzip, + tomlplusplus, version ? "git", }: stdenv.mkDerivation { @@ -14,6 +15,11 @@ stdenv.mkDerivation { inherit version; src = ../.; + patches = [ + # adds /run/current-system/sw/share/icons to the icon lookup directories + ./dirs.patch + ]; + nativeBuildInputs = [ cmake pkg-config @@ -24,6 +30,7 @@ stdenv.mkDerivation { hyprlang librsvg libzip + tomlplusplus ]; outputs = [ diff --git a/nix/dirs.patch b/nix/dirs.patch new file mode 100644 index 0000000..adc690a --- /dev/null +++ b/nix/dirs.patch @@ -0,0 +1,13 @@ +diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp +index 304ab9f..1f7e95d 100644 +--- a/libhyprcursor/hyprcursor.cpp ++++ b/libhyprcursor/hyprcursor.cpp +@@ -14,7 +14,7 @@ + using namespace Hyprcursor; + + // directories for lookup +-constexpr const std::array systemThemeDirs = {"/usr/share/icons"}; ++constexpr const std::array systemThemeDirs = {"/usr/share/icons", "/run/current-system/sw/share/icons"}; + constexpr const std::array userThemeDirs = {"/.local/share/icons", "/.icons"}; + + // diff --git a/tests/test.c b/tests/c_test.c similarity index 58% rename from tests/test.c rename to tests/c_test.c index d5fa80a..7561fd0 100644 --- a/tests/test.c +++ b/tests/c_test.c @@ -1,19 +1,46 @@ +/* + hyprlang-test in C. + Renders a cursor shape to /tmp at 48px + + For better explanations, see the cpp tests. +*/ + #include #include #include +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(NULL); + 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 || shapeData->len <= 0) { + printf("failed querying left_ptr\n"); + return 1; + } + + printf("left_ptr images: %d\n", shapeData->len); + + for (size_t i = 0; i < shapeData->len; ++i) { + printf("left_ptr image size: %d\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"); diff --git a/tests/full_rendering.cpp b/tests/full_rendering.cpp new file mode 100644 index 0000000..b784ea1 --- /dev/null +++ b/tests/full_rendering.cpp @@ -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 +#include + +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(); +} \ No newline at end of file diff --git a/tests/only_metadata.cpp b/tests/only_metadata.cpp new file mode 100644 index 0000000..2f73c77 --- /dev/null +++ b/tests/only_metadata.cpp @@ -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 +#include + +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; +} \ No newline at end of file diff --git a/tests/test.cpp b/tests/test.cpp deleted file mode 100644 index 95377d7..0000000 --- a/tests/test.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include - -int main(int argc, char** argv) { - Hyprcursor::CHyprcursorManager mgr(nullptr); - - if (!mgr.valid()) { - std::cout << "mgr is invalid\n"; - return 1; - } - - 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(); -} \ No newline at end of file