pax_global_header00006660000000000000000000000064146325143550014522gustar00rootroot0000000000000052 comment=7f4eaa029d41a8069a355ce6457f2628c3cd8981 libhyprcursor-0.1.9/000077500000000000000000000000001463251435500144405ustar00rootroot00000000000000libhyprcursor-0.1.9/.clang-format000066400000000000000000000034161463251435500170170ustar00rootroot00000000000000--- Language: Cpp BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveMacros: true AlignConsecutiveAssignments: true AlignEscapedNewlines: Right AlignOperands: false AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllConstructorInitializersOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BreakBeforeBraces: Attach BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon ColumnLimit: 180 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: false IncludeBlocks: Preserve IndentCaseLabels: true IndentWidth: 4 PointerAlignment: Left ReflowComments: false SortIncludes: false SortUsingDeclarations: false SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: true SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Auto TabWidth: 4 UseTab: Never AllowShortEnumsOnASingleLine: false BraceWrapping: AfterEnum: false AlignConsecutiveDeclarations: AcrossEmptyLines NamespaceIndentation: All libhyprcursor-0.1.9/.github/000077500000000000000000000000001463251435500160005ustar00rootroot00000000000000libhyprcursor-0.1.9/.github/workflows/000077500000000000000000000000001463251435500200355ustar00rootroot00000000000000libhyprcursor-0.1.9/.github/workflows/ci.yaml000066400000000000000000000024641463251435500213220ustar00rootroot00000000000000name: Build Hyprland on: [push, pull_request, workflow_dispatch] jobs: build: runs-on: ubuntu-latest container: image: archlinux steps: - name: Checkout repository actions uses: actions/checkout@v4 with: sparse-checkout: .github/actions - name: Get required pkgs 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 tomlplusplus - name: Install hyprlang run: | git clone https://github.com/hyprwm/hyprlang --recursive cd hyprlang cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build cmake --build ./build --config Release --target hyprlang -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` cmake --install build - name: Build hyprcursor run: | 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 --install ./build libhyprcursor-0.1.9/.gitignore000066400000000000000000000000201463251435500164200ustar00rootroot00000000000000.vscode/ build/ libhyprcursor-0.1.9/CMakeLists.txt000066400000000000000000000064071463251435500172070ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.19) set(HYPRCURSOR_VERSION "0.1.9") add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}") project(hyprcursor VERSION ${HYPRCURSOR_VERSION} DESCRIPTION "A library and toolkit for the Hyprland cursor format" ) include(CTest) include(GNUInstallDirs) set(PREFIX ${CMAKE_INSTALL_PREFIX}) set(INCLUDE ${CMAKE_INSTALL_FULL_INCLUDEDIR}) set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR}) 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 tomlplusplus) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) message(STATUS "Configuring hyprcursor in Debug") add_compile_definitions(HYPRLAND_DEBUG) else() 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") 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_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-builtin-macro-redefined) endif() # hyprcursor-util 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_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_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) libhyprcursor-0.1.9/LICENSE000066400000000000000000000027371463251435500154560ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2024, Hypr Development Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. libhyprcursor-0.1.9/README.md000066400000000000000000000026001463251435500157150ustar00rootroot00000000000000## hyprcursor The hyprland cursor format, library and utilities. ## Why? XCursor sucks, and we still use it today. - Scaling of XCursors is horrible - XCursor does not support vector cursors - XCursor is ridiculously space-inefficient Hyprcursor fixes all three. It's an efficient cursor theme format that doesn't suck as much. ### Notable advantages over XCursor - Automatic scaling according to a configurable, per-cursor method. - Support for SVG cursors - Way more space-efficient. As an example, Bibata-XCursor is 44.1MB, while it's 6.6MB in hyprcursor. ## Tools ### hyprcursor-util Utility for creating hyprcursor themes. See its readme in `hyprcursor-util/` ### libhyprcursor The library to use for implementing hyprcursors in your compositor or app. It provides C and C++ bindings. ### Examples 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: - hyprlang >= 0.4.2 - cairo - libzip - librsvg ### 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` ``` Install with: ```sh sudo cmake --install build ``` libhyprcursor-0.1.9/docs/000077500000000000000000000000001463251435500153705ustar00rootroot00000000000000libhyprcursor-0.1.9/docs/DEVELOPERS.md000066400000000000000000000003641463251435500173650ustar00rootroot00000000000000## Usage Include `hyprcursor/hyprcursor.h` or `.hpp` depending on your language. Read the comments of the API functions Have fun :) Doxygen soon I hope :P All props exposed are also explained in MAKING_THEMES.md ## Examples See `tests/`.libhyprcursor-0.1.9/docs/END_USERS.md000066400000000000000000000006361463251435500173060ustar00rootroot00000000000000## Using a hyprcursor theme Download a hyprcursor theme and extract it to a new directory in `~/.local/share/icons`. Make sure the first directory contains a manifest, for example: ```s ~/.local/share/icons/myCursorTheme/manifest.hl ``` ## Overriding a theme Set the `HYPRCURSOR_THEME` env to your theme directory, so for example to get the above to always load, use `export HYPRCURSOR_THEME = myCursorTheme`.libhyprcursor-0.1.9/docs/MAKING_THEMES.md000066400000000000000000000060221463251435500177250ustar00rootroot00000000000000## Creating a theme Familiarize yourself with the README of `hyprcursor-util`. ## Creating a theme from an XCursor theme Download an XCursor theme, extract it, and then use `--extract`, and then on the resulting output, `--create`. Before `--create`, you probably should walk through the `manifest.hl` and all the `meta.hl` files to make sure they're correct, and adjust them to your taste. ## Creating a theme from scratch The directory structure looks like this: ```ini directory ┣ manifest.hl ┗ hyprcursors ┣ left_ptr ┣ image32.png ┣ image64.png ┗ meta.hl ┣ hand ┣ image32.png ┣ image64.png ┗ meta.hl ... ``` ### Manifest The manifest describes your theme, in hyprlang: ```ini name = My theme! description = Very cool! version = 0.1 cursors_directory = hyprcursors # has to match the directory in the structure ``` ### Cursors Each cursor image is a separate directory. In it, multiple size variations can be put. `meta.hl` describes the cursor: ```ini # what resize algorithm to use when a size is requested # that doesn't match any of your predefined ones. # available: bilinear, nearest, none. None will pick the closest. Nearest is nearest neighbor. 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 # 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. define_override = arrow define_override = default # define your size variants. # Multiple size variants for the same size are treated as an animation. define_size = 64, image64.png define_size = 32, image32.png # If you want to animate it, add a timeout in ms at the end: # define_size = 64, anim1.png, 500 # define_size = 64, anim2.png, 500 # define_size = 64, anim3.png, 500 # define_size = 64, anim4.png, 500 ``` Supported cursor image types are png and svg. 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. 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.libhyprcursor-0.1.9/flake.lock000066400000000000000000000030741463251435500164000ustar00rootroot00000000000000{ "nodes": { "hyprlang": { "inputs": { "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1713121246, "narHash": "sha256-502X0Q0fhN6tJK7iEUA8CghONKSatW/Mqj4Wappd++0=", "owner": "hyprwm", "repo": "hyprlang", "rev": "78fcaa27ae9e1d782faa3ff06c8ea55ddce63706", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprlang", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1712963716, "narHash": "sha256-WKm9CvgCldeIVvRz87iOMi8CFVB1apJlkUT4GGvA0iM=", "owner": "NixOS", "repo": "nixpkgs", "rev": "cfd6b5fc90b15709b780a5a1619695a88505a176", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "hyprlang": "hyprlang", "nixpkgs": "nixpkgs", "systems": "systems" } }, "systems": { "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", "version": 7 } libhyprcursor-0.1.9/flake.nix000066400000000000000000000017531463251435500162500ustar00rootroot00000000000000{ description = "The hyprland cursor format, library and utilities"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; systems.url = "github:nix-systems/default-linux"; hyprlang = { url = "github:hyprwm/hyprlang"; inputs.systems.follows = "systems"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, systems, ... } @ inputs: let inherit (nixpkgs) lib; eachSystem = lib.genAttrs (import systems); pkgsFor = eachSystem (system: import nixpkgs { localSystem.system = system; overlays = with self.overlays; [default]; }); in { overlays = import ./nix/overlays.nix {inherit inputs lib;}; packages = eachSystem (system: { default = self.packages.${system}.hyprcursor; inherit (pkgsFor.${system}) hyprcursor; }); checks = eachSystem (system: self.packages.${system}); formatter = eachSystem (system: pkgsFor.${system}.alejandra); }; } libhyprcursor-0.1.9/hyprcursor-util/000077500000000000000000000000001463251435500176335ustar00rootroot00000000000000libhyprcursor-0.1.9/hyprcursor-util/README.md000066400000000000000000000017311463251435500211140ustar00rootroot00000000000000## hyprcursor-util A utility to compile, pack, unpack, etc, hyprcursor and xcursor themes. ## Runtime deps - xcur2png ## States Cursor themes can be in 3 states: - compiled hyprcursor - these can be used by apps / compositors. - compiled xcursor - these can be used by xcursor - working state - an easy to navigate mode where every cursor is a png / svg, and all the meta is in files. ## Commands `--create | -c [path]` -> create a compiled hyprcursor theme from a working state `--extract | -x [path]` -> extract an xcursor theme into a working state both commands support `--output | -o` to specify an output directory. For safety reasons, **do not use this on versions below 0.1.1** as it will nuke the specified directory without asking. Since v0.1.2, this directory is the parent, the theme will be written to a subdirectory in it called `$ACTION_$NAME`. ### Flags `--resize [mode]` - for `extract`: specify a default resize algorithm for shapes. Default is `none`.libhyprcursor-0.1.9/hyprcursor-util/src/000077500000000000000000000000001463251435500204225ustar00rootroot00000000000000libhyprcursor-0.1.9/hyprcursor-util/src/main.cpp000066400000000000000000000407231463251435500220600ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include "internalSharedTypes.hpp" #include "manifest.hpp" #include "meta.hpp" #ifndef ZIP_LENGTH_TO_END #define ZIP_LENGTH_TO_END -1 #endif enum eOperation { OPERATION_CREATE = 0, OPERATION_EXTRACT = 1, }; eHyprcursorResizeAlgo explicitResizeAlgo = HC_RESIZE_INVALID; struct XCursorConfigEntry { int size = 0, hotspotX = 0, hotspotY = 0, delay = 0; std::string image; }; 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 bool promptForDeletion(const std::string& path) { bool emptyDirectory = !std::filesystem::exists(path); if (!emptyDirectory) { const auto IT = std::filesystem::directory_iterator(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 + "/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); } std::cout << "About to delete (recursively) " << path << ", are you sure? [Y/n]\n"; std::string result; std::cin >> result; if (result != "Y" && result != "Y\n" && result != "y\n" && result != "y") { std::cout << "Abort.\n"; exit(1); return false; } std::filesystem::remove_all(path); return true; } 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_); CManifest manifest(path + "/manifest"); const auto PARSERESULT = manifest.parse(); if (PARSERESULT.has_value()) return "couldn't parse manifest: " + *PARSERESULT; const std::string THEMENAME = manifest.parsedData.name; std::string out = (out_.empty() ? path.substr(0, path.find_last_of('/')) : out_) + "/theme_" + THEMENAME; const std::string CURSORSSUBDIR = manifest.parsedData.cursorsDirectory; const std::string CURSORDIR = path + "/" + CURSORSSUBDIR; if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR)) return "manifest: cursors_directory missing or empty"; // iterate over the directory and record all cursors for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) { 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_\\-\\.]"; const auto METAPATH = dir.path().string() + "/meta"; auto& SHAPE = currentTheme.shapes.emplace_back(std::make_unique()); // CMeta meta{METAPATH, true, true}; const auto PARSERESULT2 = meta.parse(); if (PARSERESULT2.has_value()) return "couldn't parse meta: " + *PARSERESULT2; for (auto& i : meta.parsedData.definedSizes) { SHAPE->images.push_back(SCursorImage{i.file, i.size, i.delayMs}); } SHAPE->overrides = meta.parsedData.overrides; // check if we have at least one image. for (auto& i : SHAPE->images) { if (SHAPE->shapeType == SHAPE_INVALID) { if (i.filename.ends_with(".svg")) SHAPE->shapeType = SHAPE_SVG; 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"; SHAPE->shapeType = SHAPE_PNG; } } else { if (SHAPE->shapeType == SHAPE_SVG && !i.filename.ends_with(".svg")) return "meta invalid: cannot add .png files to an svg shape"; else if (SHAPE->shapeType == SHAPE_PNG && i.filename.ends_with(".svg")) return "meta invalid: cannot add .svg files to a png shape"; } if (!std::filesystem::exists(dir.path().string() + "/" + i.filename)) return "meta invalid: image " + i.filename + " does not exist"; break; } if (SHAPE->images.empty()) return "meta invalid: no images for shape " + dir.path().stem().string(); SHAPE->directory = dir.path().stem().string(); 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"; } // create output fs structure if (!std::filesystem::exists(out)) std::filesystem::create_directory(out); else { // clear the entire thing, avoid melting themes together promptForDeletion(out); std::filesystem::create_directory(out); } // manifest is copied 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) { const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape->directory; const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape->directory + ".hlc"; int errp = 0; zip_t* zip = zip_open(OUTPUTFILE.c_str(), ZIP_CREATE | ZIP_EXCL, &errp); if (!zip) { zip_error_t ziperror; zip_error_init_with_code(&ziperror, errp); return "Failed to open " + OUTPUTFILE + " for writing: " + zip_error_strerror(&ziperror); } // add meta.hl 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 " + 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 image for (auto& i : shape->images) { 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) return "(2) failed to add image " + i.filename + " to hlc"; std::cout << "Added image " << i.filename << " to shape " << shape->directory << "\n"; } // close zip and write if (zip_close(zip) < 0) { 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"; return {}; } static std::string spawnSync(const std::string& cmd) { std::array buffer; std::string result; const std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); if (!pipe) return ""; while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { result += buffer.data(); } return result; } static std::optional extractXTheme(const std::string& xpath_, const std::string& out_) { if (!spawnSync("xcur2png --help 2>&1").contains("xcursor")) return "missing dependency: -x requires xcur2png."; if (!std::filesystem::exists(xpath_) || !std::filesystem::exists(xpath_ + "/cursors")) return "input path does not exist or is not an xcursor theme"; const std::string xpath = std::filesystem::canonical(xpath_); std::string out = (out_.empty() ? xpath.substr(0, xpath.find_last_of('/') + 1) : out_) + "/extracted_" + xpath.substr(xpath.find_last_of('/') + 1) + "/"; // create output fs structure if (!std::filesystem::exists(out)) std::filesystem::create_directory(out); else { // clear the entire thing, avoid melting themes together promptForDeletion(out); std::filesystem::create_directory(out); } // write a boring manifest std::ofstream manifest(out + "/manifest.hl", std::ios::trunc); if (!manifest.good()) return "failed writing manifest"; manifest << "name = Extracted Theme\ndescription = Automatically extracted with hyprcursor-util\nversion = 0.1\ncursors_directory = hyprcursors\n"; manifest.close(); // make a cursors dir std::filesystem::create_directory(out + "/hyprcursors/"); // create a temp extract dir std::filesystem::create_directory("/tmp/hyprcursor-util/"); // write all cursors for (auto& xcursor : std::filesystem::directory_iterator(xpath + "/cursors/")) { // ignore symlinks, we'll write them to the meta.hl file. if (!xcursor.is_regular_file() || xcursor.is_symlink()) continue; const auto CURSORDIR = out + "/hyprcursors/" + xcursor.path().stem().string(); std::filesystem::create_directory(CURSORDIR); 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", std::filesystem::canonical(xcursor.path()).string())); // read the config std::vector entries; std::ifstream xconfig("/tmp/hyprcursor-util/" + xcursor.path().stem().string() + ".conf"); if (!xconfig.good()) return "Failed reading xconfig for " + xcursor.path().string(); std::string line = ""; while (std::getline(xconfig, line)) { if (line.starts_with("#")) continue; auto& ENTRY = entries.emplace_back(); // extract try { std::string curval = line.substr(0, line.find_first_of('\t')); ENTRY.size = std::stoi(curval); line = line.substr(line.find_first_of('\t') + 1); curval = line.substr(0, line.find_first_of('\t')); ENTRY.hotspotX = std::stoi(curval); line = line.substr(line.find_first_of('\t') + 1); curval = line.substr(0, line.find_first_of('\t')); ENTRY.hotspotY = std::stoi(curval); line = line.substr(line.find_first_of('\t') + 1); curval = line.substr(0, line.find_first_of('\t')); ENTRY.image = curval; line = line.substr(line.find_first_of('\t') + 1); curval = line.substr(0, line.find_first_of('\t')); ENTRY.delay = std::stoi(curval); } catch (std::exception& e) { return "Failed reading xconfig " + xcursor.path().string() + " because of " + e.what(); } std::cout << "Extracted " << xcursor.path().stem().string() << " at size " << ENTRY.size << "\n"; } if (entries.empty()) return "Empty xcursor " + xcursor.path().string(); // copy pngs for (auto& extracted : std::filesystem::directory_iterator("/tmp/hyprcursor-util")) { if (extracted.path().string().ends_with(".conf")) continue; std::filesystem::copy(extracted, CURSORDIR + "/"); } // write a meta.hl std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == HC_RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo)); // find hotspot from first entry metaString += std::format("hotspot_x = {:.2f}\nhotspot_y = {:.2f}\n\n", (float)entries[0].hotspotX / (float)entries[0].size, (float)entries[0].hotspotY / (float)entries[0].size); // define all sizes for (auto& entry : entries) { const auto ENTRYSTEM = entry.image.substr(entry.image.find_last_of('/') + 1); metaString += std::format("define_size = {}, {}, {}\n", entry.size, ENTRYSTEM, entry.delay); } metaString += "\n"; // define overrides, scan for symlinks for (auto& xcursor2 : std::filesystem::directory_iterator(xpath + "/cursors/")) { if (!xcursor2.is_symlink()) continue; if (std::filesystem::canonical(xcursor2) != std::filesystem::canonical(xcursor)) continue; // this sym points to us metaString += std::format("define_override = {}\n", xcursor2.path().stem().string()); } // meta done, write std::ofstream meta(CURSORDIR + "/meta.hl", std::ios::trunc); meta << metaString; meta.close(); } std::filesystem::remove_all("/tmp/hyprcursor-util/"); return {}; } int main(int argc, char** argv, char** envp) { if (argc < 2) { std::cerr << "Not enough args.\n"; return 1; } eOperation op = OPERATION_CREATE; std::string path = "", out = ""; for (size_t i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "-v" || arg == "--version") { std::cout << "hyprcursor-util, built from v" << HYPRCURSOR_VERSION << "\n"; exit(0); } if (i == 1) { // mode if (arg == "--create" || arg == "-c") { op = OPERATION_CREATE; if (argc < 3) { std::cerr << "Missing path for create.\n"; return 1; } path = argv[++i]; } else if (arg == "--extract" || arg == "-x") { op = OPERATION_EXTRACT; if (argc < 3) { std::cerr << "Missing path for extract.\n"; return 1; } path = argv[++i]; } else { std::cerr << "Invalid mode.\n"; return 1; } continue; } if (arg == "-o" || arg == "--output") { out = argv[++i]; continue; } else if (arg == "--resize") { explicitResizeAlgo = stringToAlgo(argv[++i]); continue; } else { std::cerr << "Unknown arg: " << arg << "\n"; return 1; } } if (path.ends_with("/")) path.pop_back(); switch (op) { case OPERATION_CREATE: { const auto RET = createCursorThemeFromPath(path, out); if (RET.has_value()) { std::cerr << "Failed: " << RET.value() << "\n"; return 1; } break; } case OPERATION_EXTRACT: { const auto RET = extractXTheme(path, out); if (RET.has_value()) { std::cerr << "Failed: " << RET.value() << "\n"; return 1; } break; } default: std::cerr << "Invalid mode.\n"; return 1; } return 0; } libhyprcursor-0.1.9/hyprcursor.pc.in000066400000000000000000000004101463251435500176040ustar00rootroot00000000000000prefix=@PREFIX@ includedir=@INCLUDE@ libdir=@LIBDIR@ Name: hyprcursor URL: https://github.com/hyprwm/hyprcursor Description: A library and toolkit for the Hyprland cursor format Version: @HYPRCURSOR_VERSION@ Cflags: -I${includedir} Libs: -L${libdir} -lhyprcursor libhyprcursor-0.1.9/include/000077500000000000000000000000001463251435500160635ustar00rootroot00000000000000libhyprcursor-0.1.9/include/hyprcursor/000077500000000000000000000000001463251435500203035ustar00rootroot00000000000000libhyprcursor-0.1.9/include/hyprcursor/hyprcursor.h000066400000000000000000000063051463251435500227000ustar00rootroot00000000000000 #ifndef HYPRCURSOR_H #define HYPRCURSOR_H #ifdef __cplusplus #define CAPI extern "C" #else #define CAPI #endif #include "shared.h" struct hyprcursor_manager_t; /*! Simple struct for styles */ struct hyprcursor_cursor_style_info { /*! Shape size. 0 means "any" or "unspecified". */ unsigned int size; }; /*! Basic Hyprcursor manager. Has to be created for either a specified theme, or nullptr if you want to use a default from the env. If no env is set, picks the first found. If none found, hyprcursor_manager_valid will be false. If loading fails, hyprcursor_manager_valid will be false. The caller gets the ownership, call hyprcursor_manager_free to free this object. */ 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* */ CAPI void hyprcursor_manager_free(struct hyprcursor_manager_t* manager); /*! Returns true if the theme was successfully loaded, i.e. everything is A-OK and nothing should fail. */ CAPI int hyprcursor_manager_valid(struct hyprcursor_manager_t* manager); /*! Loads a theme at a given style, synchronously. Returns whether it succeeded. */ CAPI int hyprcursor_load_theme_style(struct hyprcursor_manager_t* manager, struct hyprcursor_cursor_style_info info); /*! Returns a hyprcursor_cursor_image_data*[] for a given cursor shape and size. The entire array needs to be freed instantly after using, see hyprcursor_cursor_image_data_free() Surfaces stay valid. 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); /*! Free a returned hyprcursor_cursor_image_data. */ CAPI void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data, int size); /*! Marks a certain style as done, allowing it to be potentially freed */ 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); #endiflibhyprcursor-0.1.9/include/hyprcursor/hyprcursor.hpp000066400000000000000000000137511463251435500232430ustar00rootroot00000000000000#pragma once #include #include #include #include "shared.h" class CHyprcursorImplementation; namespace Hyprcursor { /*! Simple struct for styles */ struct SCursorStyleInfo { /*! Shape size. 0 means "any" or "unspecified". */ unsigned int size = 0; }; /*! struct for cursor shape data */ struct SCursorShapeData { 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; }; /*! 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. Has to be created for either a specified theme, or nullptr if you want to use a default from the env. If no env is set, picks the first found. 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(); /*! Returns true if the theme was successfully loaded, i.e. everything is A-OK and nothing should fail. */ bool valid(); /*! Loads this theme at a given style, synchronously. Returns whether it succeeded. */ bool loadThemeStyle(const SCursorStyleInfo& info); /*! Returns the shape data struct for a given style. Once done with a style, call cursorSurfaceDone() The surfaces references stay valid until cursorSurfaceStyleDone() is called on the owning style. */ SCursorShapeData getShape(const char* shape, const SCursorStyleInfo& info) { int size = 0; SCursorImageData** images = getShapesC(size, shape, info); SCursorShapeData data; for (size_t i = 0; i < size; ++i) { SCursorImageData image; image.delay = images[i]->delay; image.size = images[i]->size; image.surface = images[i]->surface; image.hotspotX = images[i]->hotspotX; image.hotspotY = images[i]->hotspotY; data.images.push_back(image); free(images[i]); } free(images); 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; bool allowDefaultFallback = true; PHYPRCURSORLOGFUNC logFn = nullptr; friend class CHyprcursorImplementation; }; }libhyprcursor-0.1.9/include/hyprcursor/shared.h000066400000000000000000000027151463251435500217270ustar00rootroot00000000000000#include #ifndef HYPRCURSOR_SHARED_H #define HYPRCURSOR_SHARED_H /*! struct for a single cursor image */ struct SCursorImageData { cairo_surface_t* surface; int size; int delay; int hotspotX; int hotspotY; }; 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 libhyprcursor-0.1.9/libhyprcursor/000077500000000000000000000000001463251435500173475ustar00rootroot00000000000000libhyprcursor-0.1.9/libhyprcursor/Log.hpp000066400000000000000000000007561463251435500206110ustar00rootroot00000000000000#pragma once #include #include #include #include namespace Debug { inline bool quiet = false; inline bool verbose = false; template void log(eHyprcursorLogLevel level, PHYPRCURSORLOGFUNC fn, const std::string& fmt, Args&&... args) { if (!fn) return; const std::string LOG = std::vformat(fmt, std::make_format_args(args...)); fn(level, (char*)LOG.c_str()); } };libhyprcursor-0.1.9/libhyprcursor/VarList.cpp000066400000000000000000000031221463251435500214350ustar00rootroot00000000000000#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; }libhyprcursor-0.1.9/libhyprcursor/VarList.hpp000066400000000000000000000031501463251435500214430ustar00rootroot00000000000000#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; };libhyprcursor-0.1.9/libhyprcursor/hyprcursor.cpp000066400000000000000000000645451463251435500223110ustar00rootroot00000000000000#include "hyprcursor/hyprcursor.hpp" #include "internalSharedTypes.hpp" #include "internalDefines.hpp" #include #include #include #include #include #include #include #include "manifest.hpp" #include "meta.hpp" #include "Log.hpp" using namespace Hyprcursor; // directories for lookup constexpr const std::array systemThemeDirs = {"/usr/share/icons"}; constexpr const std::array userThemeDirs = {"/.local/share/icons", "/.icons"}; // static std::string themeNameFromEnv(PHYPRCURSORLOGFUNC logfn) { const auto ENV = getenv("HYPRCURSOR_THEME"); if (!ENV) { Debug::log(HC_LOG_INFO, logfn, "themeNameFromEnv: env unset"); return ""; } return std::string{ENV}; } static bool pathAccessible(const std::string& path) { try { if (!std::filesystem::exists(path)) return false; } catch (std::exception& e) { return false; } return true; } 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"); if (!HOMEENV) return ""; const std::string HOME{HOMEENV}; for (auto& dir : userThemeDirs) { const auto FULLPATH = HOME + 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 (!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 (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 (!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 (!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 (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, PHYPRCURSORLOGFUNC logfn, bool allowDefaultFallback) { const auto HOMEENV = getenv("HOME"); if (!HOMEENV) return ""; const std::string HOME{HOMEENV}; for (auto& dir : userThemeDirs) { const auto FULLPATH = HOME + 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 (!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; 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 (!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 (!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; Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string()); return std::filesystem::canonical(themeDir.path()).string(); } } 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) { logFn = options.logFn; allowDefaultFallback = options.allowDefaultFallback; init(themeName_); } void CHyprcursorManager::init(const char* themeName_) { std::string themeName = themeName_ ? themeName_ : ""; if (allowDefaultFallback && themeName.empty()) { // try reading from env Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find theme from env"); themeName = themeNameFromEnv(logFn); } if (allowDefaultFallback && themeName.empty()) { // try finding first, in the hierarchy 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(this, logFn); impl->themeName = themeName; impl->themeFullDir = getFullPathForThemeName(themeName, logFn, allowDefaultFallback); if (impl->themeFullDir.empty()) return; 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(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; } finalizedAndValid = true; } CHyprcursorManager::~CHyprcursorManager() { if (impl) delete impl; } bool CHyprcursorManager::valid() { return finalizedAndValid; } 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; float hotX = 0, hotY = 0; for (auto& shape : impl->theme.shapes) { if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end()) continue; hotX = shape->hotspotX; hotY = shape->hotspotY; // matched :) bool foundAny = false; for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != info.size) continue; // found size resultingImages.push_back(image.get()); foundAny = true; } if (foundAny || shape->shapeType == SHAPE_SVG /* something broke, this shouldn't happen with svg */) break; // if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error. 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)) > std::abs((int)(leader - info.size))) continue; leader = image->side; } if (leader == 13371337) { // ??? Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size?"); return nullptr; } // we found nearest size for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != leader) continue; // found size resultingImages.push_back(image.get()); foundAny = true; } if (foundAny) break; Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size (2)?"); return nullptr; } // alloc and return what we need SCursorImageData** data = (SCursorImageData**)malloc(sizeof(SCursorImageData*) * resultingImages.size()); for (size_t i = 0; i < resultingImages.size(); ++i) { data[i] = (SCursorImageData*)malloc(sizeof(SCursorImageData)); data[i]->delay = resultingImages[i]->delay; data[i]->size = resultingImages[i]->side; data[i]->surface = resultingImages[i]->cairoSurface; 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 == 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) { for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != info.size) continue; sizeFound = true; break; } // size wasn't found, let's resample. if (sizeFound) continue; SLoadedCursorImage* leader = nullptr; int leaderVal = 1000000; for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side < info.size) continue; if (image->side > leaderVal) continue; leaderVal = image->side; leader = image.get(); } if (!leader) { for (auto& image : impl->loadedShapes[shape.get()].images) { if (std::abs((int)(image->side - info.size)) > leaderVal) continue; leaderVal = image->side; leader = image.get(); } } if (!leader) { 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; 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->delay = f->delay; const auto PCAIRO = cairo_create(newImage->cairoSurface); 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); cairo_paint(PCAIRO); cairo_restore(PCAIRO); 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; cairo_scale(PCAIRO, scale, scale); 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_fill(PCAIRO); cairo_surface_flush(newImage->cairoSurface); cairo_pattern_destroy(PTN); cairo_destroy(PCAIRO); } } 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; 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->delay = f->delay; const auto PCAIRO = cairo_create(newImage->cairoSurface); cairo_save(PCAIRO); cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); cairo_paint(PCAIRO); cairo_restore(PCAIRO); GError* error = nullptr; RsvgHandle* handle = rsvg_handle_new_from_data((unsigned char*)f->data, f->dataLen, &error); if (!handle) { 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(HC_LOG_ERR, logFn, "Failed rendering svg: {}", error->message); return false; } // done cairo_surface_flush(newImage->cairoSurface); cairo_destroy(PCAIRO); } } else { Debug::log(HC_LOG_ERR, logFn, "Invalid shapetype in loadThemeStyle"); return false; } } return true; } void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { for (auto& shape : impl->theme.shapes) { if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG) continue; std::erase_if(impl->loadedShapes[shape.get()].images, [info, &shape](const auto& e) { const bool isSVG = shape->shapeType == SHAPE_SVG; const bool isArtificial = e->artificial; // clean artificial rasters made for this if (isArtificial && e->side == info.size) return true; // clean invalid non-svg rasters if (!isSVG && e->side == 0) return true; return false; }); } } void CHyprcursorManager::registerLoggingFunction(PHYPRCURSORLOGFUNC fn) { logFn = fn; } /* 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, (uint8_t*)DATA->data + DATA->readNeedle, toRead); DATA->readNeedle += toRead; return CAIRO_STATUS_SUCCESS; } /* General */ std::optional CHyprcursorImplementation::loadTheme() { if (!themeAccessible(themeFullDir)) return "Theme inaccessible"; // load manifest CManifest manifest(themeFullDir + "/manifest"); const auto PARSERESULT = manifest.parse(); 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()) { 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()]; // extract zip to raw data. 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); 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 */ int readBytes = zip_fread(meta_file, buffer, 1024 * 1024 - 1); zip_fclose(meta_file); if (readBytes < 0) { delete[] buffer; return "cursor" + cursor.path().string() + "failed to read meta"; } buffer[readBytes] = '\0'; 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")) SHAPE->shapeType = SHAPE_SVG; else if (i.filename.ends_with(".png")) SHAPE->shapeType = SHAPE_PNG; else { Debug::log(HC_LOG_WARN, logFn, "WARNING: image {} has no known extension, assuming png.", i.filename); SHAPE->shapeType = SHAPE_PNG; } } else { if (SHAPE->shapeType == SHAPE_SVG && !i.filename.ends_with(".svg")) return "meta invalid: cannot add .png files to an svg shape"; else if (SHAPE->shapeType == SHAPE_PNG && i.filename.ends_with(".svg")) return "meta invalid: cannot add .svg files to a png shape"; } // load image 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; 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) 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. */ IMAGE->dataLen = zip_fread(image_file, IMAGE->data, 1024 * 1024 - 1); zip_fclose(image_file); 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; IMAGE->data = nullptr; return "Failed reading cairoSurface, status " + std::to_string((int)STATUS); } } else { 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 = meta.parsedData.hotspotX; SHAPE->hotspotY = meta.parsedData.hotspotY; SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo); zip_discard(zip); } return {}; } std::vector CHyprcursorImplementation::getFramesFor(SCursorShape* shape, int size) { std::vector frames; for (auto& image : loadedShapes[shape].images) { if (!image->isSVG && image->side != size) continue; if (image->artificial) continue; frames.push_back(image.get()); } return frames; } libhyprcursor-0.1.9/libhyprcursor/hyprcursor_c.cpp000066400000000000000000000047071463251435500226050ustar00rootroot00000000000000#include "hyprcursor/hyprcursor.h" #include "hyprcursor/hyprcursor.hpp" using namespace Hyprcursor; 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; } int hyprcursor_manager_valid(hyprcursor_manager_t* manager) { const auto MGR = (CHyprcursorManager*)manager; return MGR->valid(); } int hyprcursor_load_theme_style(hyprcursor_manager_t* manager, hyprcursor_cursor_style_info info_) { const auto MGR = (CHyprcursorManager*)manager; SCursorStyleInfo info; info.size = info_.size; return MGR->loadThemeStyle(info); } struct SCursorImageData** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info_, int* out_size) { const auto MGR = (CHyprcursorManager*)manager; SCursorStyleInfo info; info.size = info_.size; int size = 0; struct SCursorImageData** data = MGR->getShapesC(size, shape, info); *out_size = size; return data; } void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data, int size) { for (size_t i = 0; i < size; ++i) { free(data[i]); } free(data); } void hyprcursor_style_done(hyprcursor_manager_t* manager, hyprcursor_cursor_style_info info_) { const auto MGR = (CHyprcursorManager*)manager; 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; }libhyprcursor-0.1.9/libhyprcursor/internalDefines.hpp000066400000000000000000000031501463251435500231710ustar00rootroot00000000000000#pragma once #include "internalSharedTypes.hpp" #include #include #include #include struct SLoadedCursorImage { ~SLoadedCursorImage() { if (data) delete[] (char*)data; if (artificialData) delete[] (char*)artificialData; if (cairoSurface) cairo_surface_destroy(cairoSurface); } // read stuff size_t readNeedle = 0; 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 cairo_surface_t* cairoSurface = nullptr; int side = 0; int delay = 0; // means this was created by resampling void* artificialData = nullptr; bool artificial = false; }; struct SLoadedCursorShape { std::vector> images; }; class CHyprcursorImplementation { public: CHyprcursorImplementation(Hyprcursor::CHyprcursorManager* mgr, PHYPRCURSORLOGFUNC fn) : owner(mgr), logFn(fn) { ; } Hyprcursor::CHyprcursorManager* owner = nullptr; PHYPRCURSORLOGFUNC logFn = nullptr; std::string themeName; std::string themeFullDir; SCursorTheme theme; // std::unordered_map loadedShapes; // std::optional loadTheme(); std::vector getFramesFor(SCursorShape* shape, int size); };libhyprcursor-0.1.9/libhyprcursor/internalSharedTypes.hpp000066400000000000000000000022351463251435500240520ustar00rootroot00000000000000#pragma once #include #include #include #include enum eShapeType { SHAPE_INVALID = 0, SHAPE_PNG, SHAPE_SVG, }; inline eHyprcursorResizeAlgo stringToAlgo(const std::string& s) { if (s == "none") return HC_RESIZE_NONE; if (s == "nearest") return HC_RESIZE_NEAREST; return HC_RESIZE_BILINEAR; } inline std::string algoToString(const eHyprcursorResizeAlgo a) { switch (a) { case HC_RESIZE_BILINEAR: return "bilinear"; case HC_RESIZE_NEAREST: return "nearest"; case HC_RESIZE_NONE: return "none"; default: return "none"; } return "none"; } struct SCursorImage { std::string filename; int size = 0; int delay = 0; }; struct SCursorShape { std::string directory; float hotspotX = 0, hotspotY = 0; eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NEAREST; std::vector images; std::vector overrides; eShapeType shapeType = SHAPE_INVALID; }; struct SCursorTheme { std::vector> shapes; };libhyprcursor-0.1.9/libhyprcursor/manifest.cpp000066400000000000000000000053741463251435500216720ustar00rootroot00000000000000#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; }libhyprcursor-0.1.9/libhyprcursor/manifest.hpp000066400000000000000000000014331463251435500216670ustar00rootroot00000000000000#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; };libhyprcursor-0.1.9/libhyprcursor/meta.cpp000066400000000000000000000127621463251435500210110ustar00rootroot00000000000000#include "meta.hpp" #include #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; } 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; 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(); 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(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 {}; } libhyprcursor-0.1.9/libhyprcursor/meta.hpp000066400000000000000000000015361463251435500210130ustar00rootroot00000000000000#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; };libhyprcursor-0.1.9/nix/000077500000000000000000000000001463251435500152365ustar00rootroot00000000000000libhyprcursor-0.1.9/nix/default.nix000066400000000000000000000013621463251435500174040ustar00rootroot00000000000000{ lib, stdenv, cmake, pkg-config, cairo, hyprlang, librsvg, libzip, tomlplusplus, version ? "git", }: stdenv.mkDerivation { pname = "hyprcursor"; inherit version; src = ../.; patches = [ # adds /run/current-system/sw/share/icons to the icon lookup directories ./dirs.patch ]; nativeBuildInputs = [ cmake pkg-config ]; buildInputs = [ cairo hyprlang librsvg libzip tomlplusplus ]; outputs = [ "out" "dev" "lib" ]; meta = { homepage = "https://github.com/hyprwm/hyprcursor"; description = "The hyprland cursor format, library and utilities"; license = lib.licenses.bsd3; platforms = lib.platforms.linux; mainProgram = "hyprcursor"; }; } libhyprcursor-0.1.9/nix/dirs.patch000066400000000000000000000010571463251435500172230ustar00rootroot00000000000000diff --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"}; // libhyprcursor-0.1.9/nix/overlays.nix000066400000000000000000000011621463251435500176220ustar00rootroot00000000000000{ lib, inputs, }: let mkDate = longDate: (lib.concatStringsSep "-" [ (builtins.substring 0 4 longDate) (builtins.substring 4 2 longDate) (builtins.substring 6 2 longDate) ]); in { default = inputs.self.overlays.hyprcursor; hyprcursor = lib.composeManyExtensions [ 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"); inherit (final) hyprlang; }; }) ]; } libhyprcursor-0.1.9/tests/000077500000000000000000000000001463251435500156025ustar00rootroot00000000000000libhyprcursor-0.1.9/tests/c_test.c000066400000000000000000000033271463251435500172340ustar00rootroot00000000000000/* 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_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"); 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; }libhyprcursor-0.1.9/tests/full_rendering.cpp000066400000000000000000000036251463251435500213130ustar00rootroot00000000000000 /* 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(); }libhyprcursor-0.1.9/tests/only_metadata.cpp000066400000000000000000000037231463251435500211340ustar00rootroot00000000000000 /* 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; }