New upstream version 0.1.4

This commit is contained in:
alan (NyxTrail) 2024-03-18 16:39:42 +00:00
commit dbdc288df3
28 changed files with 2316 additions and 0 deletions

65
.clang-format Normal file
View File

@ -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

33
.github/workflows/ci.yaml vendored Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.vscode/
build/

77
CMakeLists.txt Normal file
View File

@ -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)

28
LICENSE Normal file
View File

@ -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.

66
README.md Normal file
View File

@ -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
```

15
docs/DEVELOPERS.md Normal file
View File

@ -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/`.

14
docs/END_USERS.md Normal file
View File

@ -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`.

80
docs/MAKING_THEMES.md Normal file
View File

@ -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.

80
flake.lock Normal file
View File

@ -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
}

39
flake.nix Normal file
View File

@ -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);
};
}

View File

@ -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)

28
hyprcursor-util/README.md Normal file
View File

@ -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`.

View File

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

View File

@ -0,0 +1,519 @@
#include <iostream>
#include <zip.h>
#include <optional>
#include <filesystem>
#include <array>
#include <format>
#include <algorithm>
#include <hyprlang.hpp>
#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<SCursorTheme> currentTheme;
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
image.delay = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
image.filename = RHS;
try {
image.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
currentTheme->shapes.back()->images.push_back(image);
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
currentTheme->shapes.back()->overrides.push_back(V);
return result;
}
static std::optional<std::string> createCursorThemeFromPath(const std::string& path_, const std::string& out_ = {}) {
if (!std::filesystem::exists(path_))
return "input path does not exist";
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<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->addConfigValue("description", Hyprlang::STRING{""});
manifest->addConfigValue("version", Hyprlang::STRING{""});
manifest->commence();
const auto RESULT = manifest->parse();
if (RESULT.error)
return "Manifest has errors: \n" + std::string{RESULT.getError()};
} catch (const char* err) { return "failed parsing manifest: " + std::string{err}; }
const std::string THEMENAME = std::any_cast<Hyprlang::STRING>(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<Hyprlang::STRING>(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<SCursorTheme>();
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<SCursorShape>());
//
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(METAPATH.c_str(), Hyprlang::SConfigOptions{});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
const auto RESULT = meta->parse();
if (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<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
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<char, 128> buffer;
std::string result;
const std::unique_ptr<FILE, decltype(&pclose)> 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<std::string> 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<XCursorConfigEntry> 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;
}

10
hyprcursor.pc.in Normal file
View File

@ -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

View File

@ -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

View File

@ -0,0 +1,107 @@
#pragma once
#include <vector>
#include <stdlib.h>
#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<SCursorImageData> 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;
};
}

View File

@ -0,0 +1,19 @@
#include <cairo/cairo.h>
#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

53
libhyprcursor/Log.hpp Normal file
View File

@ -0,0 +1,53 @@
#pragma once
enum eLogLevel {
TRACE = 0,
INFO,
LOG,
WARN,
ERR,
CRIT,
NONE
};
#include <string>
#include <format>
#include <iostream>
namespace Debug {
inline bool quiet = false;
inline bool verbose = false;
template <typename... Args>
void log(eLogLevel level, const std::string& fmt, Args&&... args) {
#ifndef HYPRLAND_DEBUG
// don't log in release
return;
#endif
if (!verbose && level == TRACE)
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";
}
};

View File

@ -0,0 +1,677 @@
#include "hyprcursor/hyprcursor.hpp"
#include "internalSharedTypes.hpp"
#include "internalDefines.hpp"
#include <array>
#include <filesystem>
#include <hyprlang.hpp>
#include <zip.h>
#include <cstring>
#include <algorithm>
#include <librsvg/rsvg.h>
#include "Log.hpp"
using namespace Hyprcursor;
// directories for lookup
constexpr const std::array<const char*, 1> systemThemeDirs = {"/usr/share/icons"};
constexpr const std::array<const char*, 2> 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<SLoadedCursorImage*> 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<SLoadedCursorImage>());
newImage->artificial = true;
newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
newImage->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<SLoadedCursorImage>());
newImage->artificial = true;
newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
newImage->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<std::string> CHyprcursorImplementation::loadTheme() {
if (!themeAccessible(themeFullDir))
return "Theme inaccessible";
currentTheme = &theme;
// load manifest
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) {
Debug::log(ERR, "Failed parsing manifest due to {}", err);
return std::string{"failed: "} + err;
}
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(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<SCursorShape>());
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<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(buffer, Hyprlang::SConfigOptions{.pathIsStream = true});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
meta->parse();
} catch (const char* err) { return "failed parsing meta: " + std::string{err}; }
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<SLoadedCursorImage>()).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<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
zip_discard(zip);
}
return {};
}
std::vector<SLoadedCursorImage*> CHyprcursorImplementation::getFramesFor(SCursorShape* shape, int size) {
std::vector<SLoadedCursorImage*> 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;
}

View File

@ -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);
}

View File

@ -0,0 +1,51 @@
#pragma once
#include "internalSharedTypes.hpp"
#include <optional>
#include <cairo/cairo.h>
#include <unordered_map>
#include <memory>
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<std::unique_ptr<SLoadedCursorImage>> images;
};
class CHyprcursorImplementation {
public:
std::string themeName;
std::string themeFullDir;
SCursorTheme theme;
//
std::unordered_map<SCursorShape*, SLoadedCursorShape> loadedShapes;
//
std::optional<std::string> loadTheme();
std::vector<SLoadedCursorImage*> getFramesFor(SCursorShape* shape, int size);
};

View File

@ -0,0 +1,55 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
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<SCursorImage> images;
std::vector<std::string> overrides;
eShapeType shapeType = SHAPE_INVALID;
};
struct SCursorTheme {
std::vector<std::unique_ptr<SCursorShape>> shapes;
};

42
nix/default.nix Normal file
View File

@ -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";
};
}

23
nix/overlays.nix Normal file
View File

@ -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;
};
})
];
}

37
tests/test.c Normal file
View File

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

36
tests/test.cpp Normal file
View File

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