From dbdc288df37ac7dfa7f517a3e6645e3da91be229 Mon Sep 17 00:00:00 2001 From: "alan (NyxTrail)" Date: Mon, 18 Mar 2024 16:39:42 +0000 Subject: [PATCH] New upstream version 0.1.4 --- .clang-format | 65 +++ .github/workflows/ci.yaml | 33 ++ .gitignore | 2 + CMakeLists.txt | 77 +++ LICENSE | 28 + README.md | 66 +++ docs/DEVELOPERS.md | 15 + docs/END_USERS.md | 14 + docs/MAKING_THEMES.md | 80 +++ flake.lock | 80 +++ flake.nix | 39 ++ hyprcursor-util/CMakeLists.txt | 24 + hyprcursor-util/README.md | 28 + hyprcursor-util/internalSharedTypes.hpp | 1 + hyprcursor-util/src/main.cpp | 519 ++++++++++++++++++ hyprcursor.pc.in | 10 + include/hyprcursor/hyprcursor.h | 86 +++ include/hyprcursor/hyprcursor.hpp | 107 ++++ include/hyprcursor/shared.h | 19 + libhyprcursor/Log.hpp | 53 ++ libhyprcursor/hyprcursor.cpp | 677 ++++++++++++++++++++++++ libhyprcursor/hyprcursor_c.cpp | 49 ++ libhyprcursor/internalDefines.hpp | 51 ++ libhyprcursor/internalSharedTypes.hpp | 55 ++ nix/default.nix | 42 ++ nix/overlays.nix | 23 + tests/test.c | 37 ++ tests/test.cpp | 36 ++ 28 files changed, 2316 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/DEVELOPERS.md create mode 100644 docs/END_USERS.md create mode 100644 docs/MAKING_THEMES.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hyprcursor-util/CMakeLists.txt create mode 100644 hyprcursor-util/README.md create mode 120000 hyprcursor-util/internalSharedTypes.hpp create mode 100644 hyprcursor-util/src/main.cpp create mode 100644 hyprcursor.pc.in create mode 100644 include/hyprcursor/hyprcursor.h create mode 100644 include/hyprcursor/hyprcursor.hpp create mode 100644 include/hyprcursor/shared.h create mode 100644 libhyprcursor/Log.hpp create mode 100644 libhyprcursor/hyprcursor.cpp create mode 100644 libhyprcursor/hyprcursor_c.cpp create mode 100644 libhyprcursor/internalDefines.hpp create mode 100644 libhyprcursor/internalSharedTypes.hpp create mode 100644 nix/default.nix create mode 100644 nix/overlays.nix create mode 100644 tests/test.c create mode 100644 tests/test.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +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 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..05a5431 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,33 @@ +name: 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 + + - 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e524d79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7d1872e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,77 @@ +cmake_minimum_required(VERSION 3.19) + +set(HYPRCURSOR_VERSION "0.1.4") +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.0 libzip cairo librsvg-2.0) + +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-macro-redefined) +endif() + +# hyprcursor-util +add_subdirectory(hyprcursor-util) + +install(TARGETS 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_test_c "tests/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(DIRECTORY "include/hyprcursor" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} DIRECTORY_PERMISSIONS + OWNER_WRITE OWNER_READ OWNER_EXECUTE + GROUP_WRITE GROUP_READ GROUP_EXECUTE + WORLD_WRITE WORLD_READ WORLD_EXECUTE) +install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d405623 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..85dbf8b --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +## 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 +``` diff --git a/docs/DEVELOPERS.md b/docs/DEVELOPERS.md new file mode 100644 index 0000000..dbaf4b0 --- /dev/null +++ b/docs/DEVELOPERS.md @@ -0,0 +1,15 @@ +## 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/`. \ No newline at end of file diff --git a/docs/END_USERS.md b/docs/END_USERS.md new file mode 100644 index 0000000..cb14940 --- /dev/null +++ b/docs/END_USERS.md @@ -0,0 +1,14 @@ +## 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`. \ No newline at end of file diff --git a/docs/MAKING_THEMES.md b/docs/MAKING_THEMES.md new file mode 100644 index 0000000..c0c04fd --- /dev/null +++ b/docs/MAKING_THEMES.md @@ -0,0 +1,80 @@ +## 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. +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. + +Please note animated svgs are not supported, you need to add a separate svg for every frame. \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..599b845 --- /dev/null +++ b/flake.lock @@ -0,0 +1,80 @@ +{ + "nodes": { + "hyprlang": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1709914708, + "narHash": "sha256-bR4o3mynoTa1Wi4ZTjbnsZ6iqVcPGriXp56bZh5UFTk=", + "owner": "hyprwm", + "repo": "hyprlang", + "rev": "a685493fdbeec01ca8ccdf1f3655c044a8ce2fe2", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprlang", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1708475490, + "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0e74ca98a74bc7270d28838369593635a5db3260", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "hyprlang": "hyprlang", + "nixpkgs": "nixpkgs", + "systems": "systems_2" + } + }, + "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" + } + }, + "systems_2": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..bcee089 --- /dev/null +++ b/flake.nix @@ -0,0 +1,39 @@ +{ + 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.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); + }; +} diff --git a/hyprcursor-util/CMakeLists.txt b/hyprcursor-util/CMakeLists.txt new file mode 100644 index 0000000..81d2847 --- /dev/null +++ b/hyprcursor-util/CMakeLists.txt @@ -0,0 +1,24 @@ +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/README.md b/hyprcursor-util/README.md new file mode 100644 index 0000000..96b7f40 --- /dev/null +++ b/hyprcursor-util/README.md @@ -0,0 +1,28 @@ +## 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`. \ No newline at end of file diff --git a/hyprcursor-util/internalSharedTypes.hpp b/hyprcursor-util/internalSharedTypes.hpp new file mode 120000 index 0000000..9402209 --- /dev/null +++ b/hyprcursor-util/internalSharedTypes.hpp @@ -0,0 +1 @@ +../libhyprcursor/internalSharedTypes.hpp \ No newline at end of file diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp new file mode 100644 index 0000000..6a622b4 --- /dev/null +++ b/hyprcursor-util/src/main.cpp @@ -0,0 +1,519 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "internalSharedTypes.hpp" + +enum eOperation { + OPERATION_CREATE = 0, + OPERATION_EXTRACT = 1, +}; + +eResizeAlgo explicitResizeAlgo = 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) && !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; +} + +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"; + + const std::string path = std::filesystem::canonical(path_); + + const auto MANIFESTPATH = path + "/manifest.hl"; + if (!std::filesystem::exists(MANIFESTPATH)) + return "manifest.hl is missing"; + + 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}; } + + const std::string THEMENAME = std::any_cast(manifest->getConfigValue("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 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 + + currentTheme = std::make_unique(); + for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) { + const auto METAPATH = dir.path().string() + "/meta.hl"; + + auto& SHAPE = currentTheme->shapes.emplace_back(std::make_unique()); + + // + std::unique_ptr meta; + + 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 (RESULT.error) + return "meta.hl has errors: \n" + std::string{RESULT.getError()}; + } catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; } + + // 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 = 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"))); + + 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(MANIFESTPATH, out + "/manifest.hl"); + + // 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 + zip_source_t* meta = zip_source_file(zip, (CURRENTCURSORSDIR + "/meta.hl").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"; + + meta = nullptr; + + // add each cursor png + for (auto& i : shape->images) { + zip_source_t* image = zip_source_file(zip, (CURRENTCURSORSDIR + "/" + i.filename).c_str(), 0, 0); + 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_error_init_with_code(&ziperror, errp); + 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 == 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; +} \ No newline at end of file diff --git a/hyprcursor.pc.in b/hyprcursor.pc.in new file mode 100644 index 0000000..35e9a52 --- /dev/null +++ b/hyprcursor.pc.in @@ -0,0 +1,10 @@ +prefix=@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 diff --git a/include/hyprcursor/hyprcursor.h b/include/hyprcursor/hyprcursor.h new file mode 100644 index 0000000..7110375 --- /dev/null +++ b/include/hyprcursor/hyprcursor.h @@ -0,0 +1,86 @@ + +#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); + +/*! + 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); + +#endif \ No newline at end of file diff --git a/include/hyprcursor/hyprcursor.hpp b/include/hyprcursor/hyprcursor.hpp new file mode 100644 index 0000000..7e8fc99 --- /dev/null +++ b/include/hyprcursor/hyprcursor.hpp @@ -0,0 +1,107 @@ +#pragma once + +#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; + }; + + /*! + 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. + */ + class CHyprcursorManager { + public: + CHyprcursorManager(const char* themeName); + ~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; + } + + /*! + Prefer getShape, this is for C compat. + */ + SCursorImageData** getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info); + + /*! + Marks a certain style as done, allowing it to be potentially freed + */ + void cursorSurfaceStyleDone(const SCursorStyleInfo&); + + private: + CHyprcursorImplementation* impl = nullptr; + bool finalizedAndValid = false; + }; + +} \ No newline at end of file diff --git a/include/hyprcursor/shared.h b/include/hyprcursor/shared.h new file mode 100644 index 0000000..d4416ad --- /dev/null +++ b/include/hyprcursor/shared.h @@ -0,0 +1,19 @@ +#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; + +#endif diff --git a/libhyprcursor/Log.hpp b/libhyprcursor/Log.hpp new file mode 100644 index 0000000..75ad948 --- /dev/null +++ b/libhyprcursor/Log.hpp @@ -0,0 +1,53 @@ +#pragma once + +enum eLogLevel { + TRACE = 0, + INFO, + LOG, + WARN, + ERR, + CRIT, + NONE +}; + +#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) + return; + + if (quiet) + return; + + 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"; + } +}; \ No newline at end of file diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp new file mode 100644 index 0000000..6e5bb0c --- /dev/null +++ b/libhyprcursor/hyprcursor.cpp @@ -0,0 +1,677 @@ +#include "hyprcursor/hyprcursor.hpp" +#include "internalSharedTypes.hpp" +#include "internalDefines.hpp" +#include +#include +#include +#include +#include +#include +#include + +#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() { + const auto ENV = getenv("HYPRCURSOR_THEME"); + if (!ENV) + return ""; + + return std::string{ENV}; +} + +static bool themeAccessible(const std::string& path) { + try { + if (!std::filesystem::exists(path + "/manifest.hl")) + return false; + + } catch (std::exception& e) { return false; } + + return true; +} + +static std::string getFirstTheme() { + // 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 (!std::filesystem::exists(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 (std::filesystem::exists(MANIFESTPATH)) + return themeDir.path().stem().string(); + } + } + + for (auto& dir : systemThemeDirs) { + const auto FULLPATH = dir; + if (!std::filesystem::exists(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 (std::filesystem::exists(MANIFESTPATH)) + return themeDir.path().stem().string(); + } + } + + return ""; +} + +static std::string getFullPathForThemeName(const std::string& name) { + const auto HOMEENV = getenv("HOME"); + if (!HOMEENV) + return ""; + + const std::string HOME{HOMEENV}; + + for (auto& dir : userThemeDirs) { + const auto FULLPATH = HOME + dir; + if (!std::filesystem::exists(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) + continue; + + const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl"; + + if (std::filesystem::exists(MANIFESTPATH)) + return std::filesystem::canonical(themeDir.path()).string(); + } + } + + for (auto& dir : systemThemeDirs) { + const auto FULLPATH = dir; + if (!std::filesystem::exists(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) + 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(); + } + } + + if (!name.empty()) // try without name + return getFullPathForThemeName(""); + + return ""; +} + +CHyprcursorManager::CHyprcursorManager(const char* themeName_) { + std::string themeName = themeName_ ? themeName_ : ""; + + if (themeName.empty()) { + // try reading from env + themeName = themeNameFromEnv(); + } + + if (themeName.empty()) { + // try finding first, in the hierarchy + themeName = getFirstTheme(); + } + + if (themeName.empty()) { + // holy shit we're done + return; + } + + // initialize theme + impl = new CHyprcursorImplementation; + impl->themeName = themeName; + impl->themeFullDir = getFullPathForThemeName(themeName); + + if (impl->themeFullDir.empty()) + return; + + Debug::log(LOG, "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()); + 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) { + 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 != RESIZE_NONE) { + Debug::log(ERR, "getSurfaceFor didn't match a size?"); + return nullptr; + } + + // find nearest + int leader = 13371337; + for (auto& image : impl->loadedShapes[shape.get()].images) { + if (std::abs((int)(image->side - info.size)) > leader) + continue; + + leader = image->side; + } + + if (leader == 13371337) { // ??? + Debug::log(ERR, "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(ERR, "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 = hotX * data[i]->size; + data[i]->hotspotY = hotY * data[i]->size; + } + + outSize = resultingImages.size(); + + return data; +} + +bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { + for (auto& shape : impl->theme.shapes) { + if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG) + continue; // don't resample NONE style cursors + + 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(ERR, "Resampling failed to find a candidate???"); + return false; + } + + const auto FRAMES = impl->getFramesFor(shape.get(), leader->side); + + 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 == 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 == 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); + + 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(ERR, "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); + return false; + } + + // done + cairo_surface_flush(newImage->cairoSurface); + cairo_destroy(PCAIRO); + } + } else { + Debug::log(ERR, "Invalid shapetype in loadThemeStyle"); + return false; + } + } + + return true; +} + +void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { + for (auto& shape : impl->theme.shapes) { + if (shape->resizeAlgo == 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; + }); + } +} + +/* + +Implementation + +*/ + +static std::string removeBeginEndSpacesTabs(std::string str) { + if (str.empty()) + return str; + + int countBefore = 0; + while (str[countBefore] == ' ' || str[countBefore] == '\t') { + countBefore++; + } + + int countAfter = 0; + while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) { + countAfter++; + } + + str = str.substr(countBefore, str.length() - countBefore - countAfter); + + return str; +} + +SCursorTheme* currentTheme; + +static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { + Hyprlang::CParseResult result; + const std::string VALUE = V; + + if (!VALUE.contains(",")) { + result.setError("Invalid define_size"); + return result; + } + + auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); + auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + auto DELAY = 0; + + SCursorImage image; + + if (RHS.contains(",")) { + const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(","))); + const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1)); + + try { + image.delay = std::stoull(RR); + } catch (std::exception& e) { + result.setError(e.what()); + return result; + } + + RHS = LL; + } + + image.filename = RHS; + + try { + image.size = std::stoull(LHS); + } catch (std::exception& e) { + result.setError(e.what()); + return result; + } + + currentTheme->shapes.back()->images.push_back(image); + + return result; +} + +static Hyprlang::CParseResult parseOverride(const char* C, const char* V) { + Hyprlang::CParseResult result; + const std::string VALUE = V; + + currentTheme->shapes.back()->overrides.push_back(V); + + return result; +} + +/* + +PNG reading + +*/ + +static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int len) { + const auto DATA = (SLoadedCursorImage*)data; + + if (!DATA->data) + return CAIRO_STATUS_READ_ERROR; + + size_t toRead = len > DATA->dataLen - DATA->readNeedle ? DATA->dataLen - DATA->readNeedle : len; + + std::memcpy(output, DATA->data + DATA->readNeedle, toRead); + 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; +} + +/* + +General + +*/ + +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; + } + + const std::string CURSORSSUBDIR = std::any_cast(manifest->getConfigValue("cursors_directory")); + 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()) + 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); + 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'; + + 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}; } + + delete[] buffer; + + 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"; + } + + // load image + Debug::log(TRACE, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); + auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique()).get(); + IMAGE->side = 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(TRACE, "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(LOG, "Skipping cairo load for a svg surface"); + } + } + + if (SHAPE->images.empty()) + return "meta invalid: no images for shape " + cursor.path().stem().string(); + + SHAPE->directory = cursor.path().stem().string(); + SHAPE->hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + SHAPE->hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + SHAPE->resizeAlgo = stringToAlgo(std::any_cast(meta->getConfigValue("resize_algorithm"))); + + 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; +} \ No newline at end of file diff --git a/libhyprcursor/hyprcursor_c.cpp b/libhyprcursor/hyprcursor_c.cpp new file mode 100644 index 0000000..2277bb0 --- /dev/null +++ b/libhyprcursor/hyprcursor_c.cpp @@ -0,0 +1,49 @@ +#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); +} + +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); +} \ No newline at end of file diff --git a/libhyprcursor/internalDefines.hpp b/libhyprcursor/internalDefines.hpp new file mode 100644 index 0000000..f6c1c14 --- /dev/null +++ b/libhyprcursor/internalDefines.hpp @@ -0,0 +1,51 @@ +#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; + 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: + std::string themeName; + std::string themeFullDir; + + SCursorTheme theme; + + // + std::unordered_map loadedShapes; + + // + std::optional loadTheme(); + std::vector getFramesFor(SCursorShape* shape, int size); +}; \ No newline at end of file diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp new file mode 100644 index 0000000..c5ac601 --- /dev/null +++ b/libhyprcursor/internalSharedTypes.hpp @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include + +enum eResizeAlgo { + RESIZE_INVALID = 0, + RESIZE_NONE, + RESIZE_BILINEAR, + RESIZE_NEAREST, +}; + +enum eShapeType { + SHAPE_INVALID = 0, + SHAPE_PNG, + SHAPE_SVG, +}; + +inline eResizeAlgo stringToAlgo(const std::string& s) { + if (s == "none") + return RESIZE_NONE; + if (s == "nearest") + return RESIZE_NEAREST; + return RESIZE_BILINEAR; +} + +inline std::string algoToString(const eResizeAlgo a) { + switch (a) { + case RESIZE_BILINEAR: return "bilinear"; + case RESIZE_NEAREST: return "nearest"; + case 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; + eResizeAlgo resizeAlgo = RESIZE_NEAREST; + std::vector images; + std::vector overrides; + eShapeType shapeType = SHAPE_INVALID; +}; + +struct SCursorTheme { + std::vector> shapes; +}; \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..e989bd9 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,42 @@ +{ + lib, + stdenv, + cmake, + pkg-config, + cairo, + hyprlang, + librsvg, + libzip, + version ? "git", +}: +stdenv.mkDerivation { + pname = "hyprcursor"; + inherit version; + src = ../.; + + nativeBuildInputs = [ + cmake + pkg-config + ]; + + buildInputs = [ + cairo + hyprlang + librsvg + libzip + ]; + + 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"; + }; +} diff --git a/nix/overlays.nix b/nix/overlays.nix new file mode 100644 index 0000000..0286ba1 --- /dev/null +++ b/nix/overlays.nix @@ -0,0 +1,23 @@ +{ + 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; + }; + }) + ]; +} diff --git a/tests/test.c b/tests/test.c new file mode 100644 index 0000000..749fbd3 --- /dev/null +++ b/tests/test.c @@ -0,0 +1,37 @@ +#include +#include +#include + +int main(int argc, char** argv) { + struct hyprcursor_manager_t* mgr = hyprcursor_manager_create(NULL); + + if (!mgr) { + printf("mgr null\n"); + return 1; + } + + struct hyprcursor_cursor_style_info info = {.size = 48}; + if (!hyprcursor_load_theme_style(mgr, info)) { + printf("load failed\n"); + return 1; + } + + int dataSize = 0; + hyprcursor_cursor_image_data** data = hyprcursor_get_cursor_image_data(mgr, "left_ptr", info, &dataSize); + if (data == NULL) { + printf("data failed\n"); + return 1; + } + + int ret = cairo_surface_write_to_png(data[0]->surface, "/tmp/arrowC.png"); + + hyprcursor_cursor_image_data_free(data, dataSize); + hyprcursor_style_done(mgr, info); + + if (ret) { + printf("cairo failed\n"); + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/tests/test.cpp b/tests/test.cpp new file mode 100644 index 0000000..155f120 --- /dev/null +++ b/tests/test.cpp @@ -0,0 +1,36 @@ +#include +#include + +int main(int argc, char** argv) { + Hyprcursor::CHyprcursorManager mgr(nullptr); + + Hyprcursor::SCursorStyleInfo style{.size = 48}; + + // preload size 48 for testing + if (!mgr.loadThemeStyle(style)) { + std::cout << "failed loading style\n"; + return 1; + } + + // get cursor for left_ptr + const auto SHAPEDATA = mgr.getShape("left_ptr", style); + + if (SHAPEDATA.images.empty()) { + std::cout << "no images\n"; + return 1; + } + + std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n"; + + // save to disk + const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png"); + + std::cout << "Cairo returned for write: " << RET << "\n"; + + mgr.cursorSurfaceStyleDone(style); + + if (RET) + return 1; + + return !mgr.valid(); +} \ No newline at end of file