New upstream version 0.1.7

This commit is contained in:
alan (NyxTrail) 2024-04-14 17:55:27 +00:00
parent e1ad3198ee
commit f6be58e58e
27 changed files with 1160 additions and 425 deletions

View File

@ -16,7 +16,7 @@ jobs:
run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang cairo librsvg git libzip
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang cairo librsvg git libzip tomlplusplus
- name: Install hyprlang
run: |

View File

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.19)
set(HYPRCURSOR_VERSION "0.1.5")
set(HYPRCURSOR_VERSION "0.1.7")
add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}")
project(hyprcursor
@ -20,7 +20,7 @@ configure_file(hyprcursor.pc.in hyprcursor.pc @ONLY)
set(CMAKE_CXX_STANDARD 23)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.2 libzip cairo librsvg-2.0)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.2 libzip cairo librsvg-2.0 tomlplusplus)
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring hyprcursor in Debug")
@ -54,23 +54,34 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
endif()
# hyprcursor-util
add_subdirectory(hyprcursor-util)
install(TARGETS hyprcursor)
file(GLOB_RECURSE UTILSRCFILES CONFIGURE_DEPENDS "hyprcursor-util/src/*.cpp" "include/hyprcursor/hyprcursor.hpp" "include/hyprcursor/hyprcursor.h" "include/hyprcursor/shared.h")
add_executable(hyprcursor-util ${UTILSRCFILES})
target_include_directories(hyprcursor-util
PUBLIC "./include"
PRIVATE "./libhyprcursor" "./hyprcursor-util/src"
)
target_link_libraries(hyprcursor-util PkgConfig::deps hyprcursor)
# tests
add_custom_target(tests)
add_executable(hyprcursor_test "tests/test.cpp")
target_link_libraries(hyprcursor_test PRIVATE hyprcursor)
add_test(NAME "Test libhyprcursor in C++" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test)
add_dependencies(tests hyprcursor_test)
add_executable(hyprcursor_test1 "tests/full_rendering.cpp")
target_link_libraries(hyprcursor_test1 PRIVATE hyprcursor)
add_test(NAME "Test libhyprcursor in C++ (full rendering)" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test1)
add_dependencies(tests hyprcursor_test1)
add_executable(hyprcursor_test_c "tests/test.c")
add_executable(hyprcursor_test2 "tests/only_metadata.cpp")
target_link_libraries(hyprcursor_test2 PRIVATE hyprcursor)
add_test(NAME "Test libhyprcursor in C++ (only metadata)" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test2)
add_dependencies(tests hyprcursor_test2)
add_executable(hyprcursor_test_c "tests/c_test.c")
target_link_libraries(hyprcursor_test_c PRIVATE hyprcursor)
add_test(NAME "Test libhyprcursor in C" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test_c)
add_dependencies(tests hyprcursor_test_c)
# Installation
install(TARGETS hyprcursor)
install(TARGETS hyprcursor-util)
install(DIRECTORY "include/hyprcursor" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

View File

@ -50,6 +50,8 @@ resize_algorithm = bilinear
# "hotspot" is where in your cursor the actual "click point" should be.
# this is in absolute coordinates. x+ is east, y+ is north.
# the pixel coordinates of the hotspot at size are rounded to the nearest:
# (round(size * hotspot_x), round(size * hotspot_y))
hotspot_x = 0.0 # this goes 0 - 1
hotspot_y = 0.0 # this goes 0 - 1
@ -77,4 +79,26 @@ If you are using an svg cursor, the size parameter will be ignored.
Mixing png and svg cursor images in one shape will result in an error.
Please note animated svgs are not supported, you need to add a separate svg for every frame.
All cursors are required to have an aspect ratio of 1:1.
Please note animated svgs are not supported, you need to add a separate svg for every frame.
### TOML
You are allowed to use TOML for all .hl files. Make sure to change the extension from `.hl` to `.toml`!
#### Manifest
Append `[General]` to the top, and wrap all the values in quotes.
#### Meta
Append `[General]` to the top, and wrap all values except hotspot in quotes.
Additionally, if you have multiple `define_*` keys, merge them into one like this:
```toml
define_override = 'shape1;shape2;shape3'
define_size = '24,image1.png,200;24,image2.png,200;32,image3.png,200'
```
You can put spaces around the semicolons if you prefer to.

View File

@ -7,6 +7,7 @@
hyprlang = {
url = "github:hyprwm/hyprlang";
inputs.systems.follows = "systems";
inputs.nixpkgs.follows = "nixpkgs";
};
};

View File

@ -1,24 +0,0 @@
cmake_minimum_required(VERSION 3.19)
project(
hyprcursor-util
DESCRIPTION "A utility for creating and converting hyprcursor themes"
)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.0 libzip)
add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}")
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
set(CMAKE_CXX_STANDARD 23)
add_executable(hyprcursor-util ${SRCFILES})
target_link_libraries(hyprcursor-util PkgConfig::deps)
target_include_directories(hyprcursor-util
PRIVATE
.
)
install(TARGETS hyprcursor-util)

View File

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

View File

@ -7,13 +7,15 @@
#include <algorithm>
#include <hyprlang.hpp>
#include "internalSharedTypes.hpp"
#include "manifest.hpp"
#include "meta.hpp"
enum eOperation {
OPERATION_CREATE = 0,
OPERATION_EXTRACT = 1,
};
eResizeAlgo explicitResizeAlgo = RESIZE_INVALID;
eHyprcursorResizeAlgo explicitResizeAlgo = HC_RESIZE_INVALID;
struct XCursorConfigEntry {
int size = 0, hotspotX = 0, hotspotY = 0, delay = 0;
@ -48,7 +50,7 @@ static bool promptForDeletion(const std::string& path) {
emptyDirectory = !std::count_if(std::filesystem::begin(IT), std::filesystem::end(IT), [](auto& e) { return e.is_regular_file(); });
}
if (!std::filesystem::exists(path + "/manifest.hl") && std::filesystem::exists(path) && !emptyDirectory) {
if (!std::filesystem::exists(path + "/manifest.hl") && !std::filesystem::exists(path + "/manifest.toml") && std::filesystem::exists(path) && !emptyDirectory) {
std::cout << "Refusing to remove " << path << " because it doesn't look like a hyprcursor theme.\n"
<< "Please set a valid, empty, nonexistent, or a theme directory as an output path\n";
exit(1);
@ -69,88 +71,25 @@ static bool promptForDeletion(const std::string& path) {
return true;
}
std::unique_ptr<SCursorTheme> currentTheme;
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
image.delay = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
image.filename = RHS;
try {
image.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
currentTheme->shapes.back()->images.push_back(image);
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
currentTheme->shapes.back()->overrides.push_back(V);
return result;
}
static std::optional<std::string> createCursorThemeFromPath(const std::string& path_, const std::string& out_ = {}) {
if (!std::filesystem::exists(path_))
return "input path does not exist";
SCursorTheme currentTheme;
const std::string path = std::filesystem::canonical(path_);
const auto MANIFESTPATH = path + "/manifest.hl";
if (!std::filesystem::exists(MANIFESTPATH))
return "manifest.hl is missing";
CManifest manifest(path + "/manifest");
const auto PARSERESULT = manifest.parse();
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->addConfigValue("description", Hyprlang::STRING{""});
manifest->addConfigValue("version", Hyprlang::STRING{""});
manifest->commence();
const auto RESULT = manifest->parse();
if (RESULT.error)
return "Manifest has errors: \n" + std::string{RESULT.getError()};
} catch (const char* err) { return "failed parsing manifest: " + std::string{err}; }
if (PARSERESULT.has_value())
return "couldn't parse manifest: " + *PARSERESULT;
const std::string THEMENAME = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
const std::string THEMENAME = manifest.parsedData.name;
std::string out = (out_.empty() ? path.substr(0, path.find_last_of('/') + 1) : out_) + "/theme_" + THEMENAME + "/";
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
const std::string CURSORSSUBDIR = manifest.parsedData.cursorsDirectory;
const std::string CURSORDIR = path + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
@ -158,28 +97,21 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
// iterate over the directory and record all cursors
currentTheme = std::make_unique<SCursorTheme>();
for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) {
const auto METAPATH = dir.path().string() + "/meta.hl";
const auto METAPATH = dir.path().string() + "/meta";
auto& SHAPE = currentTheme->shapes.emplace_back(std::make_unique<SCursorShape>());
auto& SHAPE = currentTheme.shapes.emplace_back(std::make_unique<SCursorShape>());
//
std::unique_ptr<Hyprlang::CConfig> meta;
CMeta meta{METAPATH, true, true};
const auto PARSERESULT2 = meta.parse();
try {
meta = std::make_unique<Hyprlang::CConfig>(METAPATH.c_str(), Hyprlang::SConfigOptions{});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
const auto RESULT = meta->parse();
if (PARSERESULT2.has_value())
return "couldn't parse meta: " + *PARSERESULT2;
if (RESULT.error)
return "meta.hl has errors: \n" + std::string{RESULT.getError()};
} catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; }
for (auto& i : meta.parsedData.definedSizes) {
SHAPE->images.push_back(SCursorImage{i.file, i.size, i.delayMs});
}
// check if we have at least one image.
for (auto& i : SHAPE->images) {
@ -209,9 +141,9 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
return "meta invalid: no images for shape " + dir.path().stem().string();
SHAPE->directory = dir.path().stem().string();
SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
SHAPE->hotspotX = meta.parsedData.hotspotX;
SHAPE->hotspotY = meta.parsedData.hotspotY;
SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo);
std::cout << "Shape " << SHAPE->directory << ": \n\toverrides: " << SHAPE->overrides.size() << "\n\tsizes: " << SHAPE->images.size() << "\n";
}
@ -226,13 +158,13 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
}
// manifest is copied
std::filesystem::copy(MANIFESTPATH, out + "/manifest.hl");
std::filesystem::copy(manifest.getPath(), out + "/manifest." + (manifest.getPath().ends_with(".hl") ? "hl" : "toml"));
// create subdir for cursors
std::filesystem::create_directory(out + "/" + CURSORSSUBDIR);
// create zips (.hlc) for each
for (auto& shape : currentTheme->shapes) {
for (auto& shape : currentTheme.shapes) {
const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape->directory;
const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape->directory + ".hlc";
int errp = 0;
@ -245,11 +177,12 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
}
// add meta.hl
zip_source_t* meta = zip_source_file(zip, (CURRENTCURSORSDIR + "/meta.hl").c_str(), 0, 0);
const auto METADIR = std::filesystem::exists(CURRENTCURSORSDIR + "/meta.hl") ? (CURRENTCURSORSDIR + "/meta.hl") : (CURRENTCURSORSDIR + "/meta.toml");
zip_source_t* meta = zip_source_file(zip, METADIR.c_str(), 0, 0);
if (!meta)
return "(1) failed to add meta " + (CURRENTCURSORSDIR + "/meta.hl") + " to hlc";
if (zip_file_add(zip, "meta.hl", meta, ZIP_FL_ENC_UTF_8) < 0)
return "(2) failed to add meta " + (CURRENTCURSORSDIR + "/meta.hl") + " to hlc";
return "(1) failed to add meta " + METADIR + " to hlc";
if (zip_file_add(zip, (std::string{"meta."} + (METADIR.ends_with(".hl") ? "hl" : "toml")).c_str(), meta, ZIP_FL_ENC_UTF_8) < 0)
return "(2) failed to add meta " + METADIR + " to hlc";
meta = nullptr;
@ -275,7 +208,7 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
}
// done!
std::cout << "Done, written " << currentTheme->shapes.size() << " shapes.\n";
std::cout << "Done, written " << currentTheme.shapes.size() << " shapes.\n";
return {};
}
@ -396,7 +329,7 @@ static std::optional<std::string> extractXTheme(const std::string& xpath_, const
}
// write a meta.hl
std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo));
std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == HC_RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo));
// find hotspot from first entry
metaString +=

View File

@ -43,6 +43,13 @@ struct hyprcursor_cursor_style_info {
*/
CAPI struct hyprcursor_manager_t* hyprcursor_manager_create(const char* theme_name);
/*!
\since 0.1.6
Same as hyprcursor_manager_create, but with a logger.
*/
CAPI struct hyprcursor_manager_t* hyprcursor_manager_create_with_logger(const char* theme_name, PHYPRCURSORLOGFUNC fn);
/*!
Free a hyprcursor_manager_t*
*/
@ -71,7 +78,8 @@ CAPI int hyprcursor_load_theme_style(struct hyprcursor_manager_t* manager, struc
Once done with a size, call hyprcursor_style_done()
*/
CAPI hyprcursor_cursor_image_data** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info, int* out_size);
CAPI hyprcursor_cursor_image_data** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info,
int* out_size);
/*!
Free a returned hyprcursor_cursor_image_data.
@ -83,4 +91,32 @@ CAPI void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data,
*/
CAPI void hyprcursor_style_done(struct hyprcursor_manager_t* manager, struct hyprcursor_cursor_style_info info);
/*!
\since 0.1.6
Registers a logging function to a hyprcursor_manager_t*
PHYPRCURSORLOGFUNC's msg is owned by the caller and will be freed afterwards.
fn can be null to remove a logger.
*/
CAPI void hyprcursor_register_logging_function(struct hyprcursor_manager_t* manager, PHYPRCURSORLOGFUNC fn);
/*!
\since 0.1.6
Returns the raw image data of a cursor shape, not rendered at all, alongside the metadata.
The object needs to be freed instantly after using, see hyprcursor_raw_shape_data_free()
*/
CAPI hyprcursor_cursor_raw_shape_data* hyprcursor_get_raw_shape_data(struct hyprcursor_manager_t* manager, char* shape);
/*!
\since 0.1.6
See hyprcursor_get_raw_shape_data.
Frees the returned object.
*/
CAPI void hyprcursor_raw_shape_data_free(hyprcursor_cursor_raw_shape_data* data);
#endif

View File

@ -2,6 +2,7 @@
#include <vector>
#include <stdlib.h>
#include <string>
#include "shared.h"
@ -28,6 +29,24 @@ namespace Hyprcursor {
std::vector<SCursorImageData> images;
};
/*!
C++ structs for hyprcursor_cursor_raw_shape_image and hyprcursor_cursor_raw_shape_data
*/
struct SCursorRawShapeImage {
std::vector<unsigned char> data;
int size = 0;
int delay = 200;
};
struct SCursorRawShapeData {
std::vector<SCursorRawShapeImage> images;
float hotspotX = 0;
float hotspotY = 0;
std::string overridenBy = "";
eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NONE;
eHyprcursorDataType type = HC_DATA_PNG;
};
/*!
Basic Hyprcursor manager.
@ -43,6 +62,10 @@ namespace Hyprcursor {
class CHyprcursorManager {
public:
CHyprcursorManager(const char* themeName);
/*!
\since 0.1.6
*/
CHyprcursorManager(const char* themeName, PHYPRCURSORLOGFUNC fn);
~CHyprcursorManager();
/*!
@ -89,19 +112,69 @@ namespace Hyprcursor {
return data;
}
/*!
\since 0.1.6
Returns the raw image data of a cursor shape, not rendered at all, alongside the metadata.
*/
SCursorRawShapeData getRawShapeData(const char* shape_) {
auto CDATA = getRawShapeDataC(shape_);
if (CDATA->overridenBy) {
SCursorRawShapeData d{.overridenBy = CDATA->overridenBy};
free(CDATA->overridenBy);
delete CDATA;
return d;
}
SCursorRawShapeData data{.hotspotX = CDATA->hotspotX, .hotspotY = CDATA->hotspotY, .overridenBy = "", .resizeAlgo = CDATA->resizeAlgo, .type = CDATA->type};
for (size_t i = 0; i < CDATA->len; ++i) {
SCursorRawShapeImageC* cimage = &CDATA->images[i];
SCursorRawShapeImage& img = data.images.emplace_back();
img.size = cimage->size;
img.delay = cimage->delay;
img.data = std::vector<unsigned char>{(unsigned char*)cimage->data, (unsigned char*)cimage->data + (std::size_t)cimage->len};
}
delete[] CDATA->images;
delete CDATA;
return data;
}
/*!
Prefer getShape, this is for C compat.
*/
SCursorImageData** getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info);
/*!
Prefer getShapeData, this is for C compat.
*/
SCursorRawShapeDataC* getRawShapeDataC(const char* shape_);
/*!
Marks a certain style as done, allowing it to be potentially freed
*/
void cursorSurfaceStyleDone(const SCursorStyleInfo&);
/*!
\since 0.1.6
Registers a logging function to this manager.
PHYPRCURSORLOGFUNC's msg is owned by the caller and will be freed afterwards.
fn can be null to unregister a logger.
*/
void registerLoggingFunction(PHYPRCURSORLOGFUNC fn);
private:
void init(const char* themeName_);
CHyprcursorImplementation* impl = nullptr;
bool finalizedAndValid = false;
PHYPRCURSORLOGFUNC logFn = nullptr;
friend class CHyprcursorImplementation;
};
}

View File

@ -16,4 +16,51 @@ struct SCursorImageData {
typedef struct SCursorImageData hyprcursor_cursor_image_data;
enum eHyprcursorLogLevel {
HC_LOG_NONE = 0,
HC_LOG_TRACE,
HC_LOG_INFO,
HC_LOG_WARN,
HC_LOG_ERR,
HC_LOG_CRITICAL,
};
enum eHyprcursorDataType {
HC_DATA_PNG = 0,
HC_DATA_SVG,
};
enum eHyprcursorResizeAlgo {
HC_RESIZE_INVALID = 0,
HC_RESIZE_NONE,
HC_RESIZE_BILINEAR,
HC_RESIZE_NEAREST,
};
struct SCursorRawShapeImageC {
void* data;
unsigned long int len;
int size;
int delay;
};
typedef struct SCursorRawShapeImageC hyprcursor_cursor_raw_shape_image;
struct SCursorRawShapeDataC {
struct SCursorRawShapeImageC* images;
unsigned long int len;
float hotspotX;
float hotspotY;
char* overridenBy;
enum eHyprcursorResizeAlgo resizeAlgo;
enum eHyprcursorDataType type;
};
typedef struct SCursorRawShapeDataC hyprcursor_cursor_raw_shape_data;
/*
msg is owned by the caller and will be freed afterwards.
*/
typedef void (*PHYPRCURSORLOGFUNC)(enum eHyprcursorLogLevel level, char* msg);
#endif

View File

@ -1,53 +1,22 @@
#pragma once
enum eLogLevel {
TRACE = 0,
INFO,
LOG,
WARN,
ERR,
CRIT,
NONE
};
#include <string>
#include <format>
#include <iostream>
#include <hyprcursor/shared.h>
namespace Debug {
inline bool quiet = false;
inline bool verbose = false;
template <typename... Args>
void log(eLogLevel level, const std::string& fmt, Args&&... args) {
#ifndef HYPRLAND_DEBUG
// don't log in release
return;
#endif
if (!verbose && level == TRACE)
void log(eHyprcursorLogLevel level, PHYPRCURSORLOGFUNC fn, const std::string& fmt, Args&&... args) {
if (!fn)
return;
if (quiet)
return;
const std::string LOG = std::vformat(fmt, std::make_format_args(args...));
if (level != NONE) {
std::cout << '[';
switch (level) {
case TRACE: std::cout << "TRACE"; break;
case INFO: std::cout << "INFO"; break;
case LOG: std::cout << "LOG"; break;
case WARN: std::cout << "WARN"; break;
case ERR: std::cout << "ERR"; break;
case CRIT: std::cout << "CRITICAL"; break;
default: break;
}
std::cout << "] ";
}
std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n";
fn(level, (char*)LOG.c_str());
}
};

55
libhyprcursor/VarList.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "VarList.hpp"
#include <ranges>
#include <algorithm>
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
CVarList::CVarList(const std::string& in, const size_t lastArgNo, const char delim, const bool removeEmpty) {
if (in.empty())
m_vArgs.emplace_back("");
std::string args{in};
size_t idx = 0;
size_t pos = 0;
std::ranges::replace_if(
args, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0);
for (const auto& s : args | std::views::split(0)) {
if (removeEmpty && s.empty())
continue;
if (++idx == lastArgNo) {
m_vArgs.emplace_back(removeBeginEndSpacesTabs(in.substr(pos)));
break;
}
pos += s.size() + 1;
m_vArgs.emplace_back(removeBeginEndSpacesTabs(std::string_view{s}.data()));
}
}
std::string CVarList::join(const std::string& joiner, size_t from, size_t to) const {
size_t last = to == 0 ? size() : to;
std::string rolling;
for (size_t i = from; i < last; ++i) {
rolling += m_vArgs[i] + (i + 1 < last ? joiner : "");
}
return rolling;
}

63
libhyprcursor/VarList.hpp Normal file
View File

@ -0,0 +1,63 @@
#pragma once
#include <functional>
#include <vector>
#include <string>
class CVarList {
public:
/** Split string into arg list
@param lastArgNo stop splitting after argv reaches maximum size, last arg will contain rest of unsplit args
@param delim if delimiter is 's', use std::isspace
@param removeEmpty remove empty args from argv
*/
CVarList(const std::string& in, const size_t maxSize = 0, const char delim = ',', const bool removeEmpty = false);
~CVarList() = default;
size_t size() const {
return m_vArgs.size();
}
std::string join(const std::string& joiner, size_t from = 0, size_t to = 0) const;
void map(std::function<void(std::string&)> func) {
for (auto& s : m_vArgs)
func(s);
}
void append(const std::string arg) {
m_vArgs.emplace_back(arg);
}
std::string operator[](const size_t& idx) const {
if (idx >= m_vArgs.size())
return "";
return m_vArgs[idx];
}
// for range-based loops
std::vector<std::string>::iterator begin() {
return m_vArgs.begin();
}
std::vector<std::string>::const_iterator begin() const {
return m_vArgs.begin();
}
std::vector<std::string>::iterator end() {
return m_vArgs.end();
}
std::vector<std::string>::const_iterator end() const {
return m_vArgs.end();
}
bool contains(const std::string& el) {
for (auto& a : m_vArgs) {
if (a == el)
return true;
}
return false;
}
private:
std::vector<std::string> m_vArgs;
};

View File

@ -3,12 +3,14 @@
#include "internalDefines.hpp"
#include <array>
#include <filesystem>
#include <hyprlang.hpp>
#include <zip.h>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <librsvg/rsvg.h>
#include "manifest.hpp"
#include "meta.hpp"
#include "Log.hpp"
using namespace Hyprcursor;
@ -18,17 +20,19 @@ 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() {
static std::string themeNameFromEnv(PHYPRCURSORLOGFUNC logfn) {
const auto ENV = getenv("HYPRCURSOR_THEME");
if (!ENV)
if (!ENV) {
Debug::log(HC_LOG_INFO, logfn, "themeNameFromEnv: env unset");
return "";
}
return std::string{ENV};
}
static bool themeAccessible(const std::string& path) {
static bool pathAccessible(const std::string& path) {
try {
if (!std::filesystem::exists(path + "/manifest.hl"))
if (!std::filesystem::exists(path))
return false;
} catch (std::exception& e) { return false; }
@ -36,7 +40,11 @@ static bool themeAccessible(const std::string& path) {
return true;
}
static std::string getFirstTheme() {
static bool themeAccessible(const std::string& path) {
return pathAccessible(path + "/manifest.hl") || pathAccessible(path + "/manifest.toml");
}
static std::string getFirstTheme(PHYPRCURSORLOGFUNC logfn) {
// try user directories first
const auto HOMEENV = getenv("HOME");
@ -47,42 +55,60 @@ static std::string getFirstTheme() {
for (auto& dir : userThemeDirs) {
const auto FULLPATH = HOME + dir;
if (!std::filesystem::exists(FULLPATH))
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
if (std::filesystem::exists(MANIFESTPATH))
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.";
if (std::filesystem::exists(MANIFESTPATH + "hl") || std::filesystem::exists(MANIFESTPATH + "toml")) {
Debug::log(HC_LOG_INFO, logfn, "getFirstTheme: found {}", themeDir.path().string());
return themeDir.path().stem().string();
}
}
}
for (auto& dir : systemThemeDirs) {
const auto FULLPATH = dir;
if (!std::filesystem::exists(FULLPATH))
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
if (std::filesystem::exists(MANIFESTPATH))
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.";
if (std::filesystem::exists(MANIFESTPATH + "hl") || std::filesystem::exists(MANIFESTPATH + "toml")) {
Debug::log(HC_LOG_INFO, logfn, "getFirstTheme: found {}", themeDir.path().string());
return themeDir.path().stem().string();
}
}
}
return "";
}
static std::string getFullPathForThemeName(const std::string& name) {
static std::string getFullPathForThemeName(const std::string& name, PHYPRCURSORLOGFUNC logfn) {
const auto HOMEENV = getenv("HOME");
if (!HOMEENV)
return "";
@ -91,103 +117,134 @@ static std::string getFullPathForThemeName(const std::string& name) {
for (auto& dir : userThemeDirs) {
const auto FULLPATH = HOME + dir;
if (!std::filesystem::exists(FULLPATH))
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (name.empty()) {
if (std::filesystem::exists(MANIFESTPATH))
return std::filesystem::canonical(themeDir.path()).string();
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
if (!std::filesystem::exists(MANIFESTPATH))
continue;
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* e) { continue; }
const std::string NAME = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
if (NAME != name)
const auto MANIFESTPATH = themeDir.path().string() + "/manifest";
if (name.empty()) {
if (std::filesystem::exists(MANIFESTPATH + ".hl") || std::filesystem::exists(MANIFESTPATH + ".toml")) {
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
}
continue;
}
CManifest manifest{MANIFESTPATH};
if (const auto R = manifest.parse(); R.has_value()) {
Debug::log(HC_LOG_ERR, logfn, "failed parsing Manifest of {}: {}", themeDir.path().string(), *R);
continue;
}
const std::string NAME = manifest.parsedData.name;
if (NAME != name && name != themeDir.path().stem().string())
continue;
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
}
}
for (auto& dir : systemThemeDirs) {
const auto FULLPATH = dir;
if (!std::filesystem::exists(FULLPATH))
if (!pathAccessible(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
continue;
}
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
if (!name.empty() && themeDir.path().stem().string() != name)
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
const auto MANIFESTPATH = themeDir.path().string() + "/manifest";
CManifest manifest{MANIFESTPATH};
if (const auto R = manifest.parse(); R.has_value()) {
Debug::log(HC_LOG_ERR, logfn, "failed parsing Manifest of {}: {}", themeDir.path().string(), *R);
continue;
}
const std::string NAME = manifest.parsedData.name;
if (NAME != name && name != themeDir.path().stem().string())
continue;
if (!themeAccessible(themeDir.path().string()))
continue;
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (std::filesystem::exists(MANIFESTPATH))
return std::filesystem::canonical(themeDir.path()).string();
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
}
}
if (!name.empty()) // try without name
return getFullPathForThemeName("");
if (!name.empty()) { // try without name
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: failed, trying without name of {}", name);
return getFullPathForThemeName("", logfn);
}
return "";
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_) {
init(themeName_);
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_, PHYPRCURSORLOGFUNC fn) {
logFn = fn;
init(themeName_);
}
void CHyprcursorManager::init(const char* themeName_) {
std::string themeName = themeName_ ? themeName_ : "";
if (themeName.empty()) {
// try reading from env
themeName = themeNameFromEnv();
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find theme from env");
themeName = themeNameFromEnv(logFn);
}
if (themeName.empty()) {
// try finding first, in the hierarchy
themeName = getFirstTheme();
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find any theme");
themeName = getFirstTheme(logFn);
}
if (themeName.empty()) {
// holy shit we're done
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: no themes matched");
return;
}
// initialize theme
impl = new CHyprcursorImplementation;
impl = new CHyprcursorImplementation(this, logFn);
impl->themeName = themeName;
impl->themeFullDir = getFullPathForThemeName(themeName);
impl->themeFullDir = getFullPathForThemeName(themeName, logFn);
if (impl->themeFullDir.empty())
return;
Debug::log(LOG, "Found theme {} at {}\n", impl->themeName, impl->themeFullDir);
Debug::log(HC_LOG_INFO, logFn, "Found theme {} at {}\n", impl->themeName, impl->themeFullDir);
const auto LOADSTATUS = impl->loadTheme();
if (LOADSTATUS.has_value()) {
Debug::log(ERR, "Theme failed to load with {}\n", LOADSTATUS.value());
Debug::log(HC_LOG_ERR, logFn, "Theme failed to load with {}\n", LOADSTATUS.value());
return;
}
@ -204,6 +261,11 @@ bool CHyprcursorManager::valid() {
}
SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info) {
if (!shape_) {
Debug::log(HC_LOG_ERR, logFn, "getShapesC: shape of nullptr is invalid");
return nullptr;
}
std::string REQUESTEDSHAPE = shape_;
std::vector<SLoadedCursorImage*> resultingImages;
@ -231,8 +293,8 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
break;
// if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error.
if (shape->resizeAlgo != RESIZE_NONE) {
Debug::log(ERR, "getSurfaceFor didn't match a size?");
if (shape->resizeAlgo != HC_RESIZE_NONE) {
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match a size?");
return nullptr;
}
@ -246,7 +308,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
}
if (leader == 13371337) { // ???
Debug::log(ERR, "getSurfaceFor didn't match any nearest size?");
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size?");
return nullptr;
}
@ -263,7 +325,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
if (foundAny)
break;
Debug::log(ERR, "getSurfaceFor didn't match any nearest size (2)?");
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size (2)?");
return nullptr;
}
@ -274,19 +336,74 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
data[i]->delay = resultingImages[i]->delay;
data[i]->size = resultingImages[i]->side;
data[i]->surface = resultingImages[i]->cairoSurface;
data[i]->hotspotX = hotX * data[i]->size;
data[i]->hotspotY = hotY * data[i]->size;
data[i]->hotspotX = std::round(hotX * (float)data[i]->size);
data[i]->hotspotY = std::round(hotY * (float)data[i]->size);
}
outSize = resultingImages.size();
Debug::log(HC_LOG_INFO, logFn, "getShapesC: found {} images for {}", outSize, shape_);
return data;
}
SCursorRawShapeDataC* CHyprcursorManager::getRawShapeDataC(const char* shape_) {
if (!shape_) {
Debug::log(HC_LOG_ERR, logFn, "getShapeDataC: shape of nullptr is invalid");
return nullptr;
}
const std::string SHAPE = shape_;
SCursorRawShapeDataC* data = new SCursorRawShapeDataC;
std::vector<SLoadedCursorImage*> resultingImages;
for (auto& shape : impl->theme.shapes) {
// if it's overridden just return the override
if (const auto IT = std::find(shape->overrides.begin(), shape->overrides.end(), SHAPE); IT != shape->overrides.end()) {
data->overridenBy = strdup(IT->c_str());
return data;
}
if (shape->directory != SHAPE)
continue;
if (!impl->loadedShapes.contains(shape.get()))
continue; // ??
// found it
for (auto& i : impl->loadedShapes[shape.get()].images) {
resultingImages.push_back(i.get());
}
data->hotspotX = shape->hotspotX;
data->hotspotY = shape->hotspotY;
data->type = shape->shapeType == SHAPE_PNG ? HC_DATA_PNG : HC_DATA_SVG;
break;
}
data->len = resultingImages.size();
data->images = new SCursorRawShapeImageC[data->len];
for (size_t i = 0; i < data->len; ++i) {
data->images[i].data = resultingImages[i]->data;
data->images[i].len = resultingImages[i]->dataLen;
data->images[i].size = resultingImages[i]->side;
data->images[i].delay = resultingImages[i]->delay;
}
return data;
}
bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
Debug::log(HC_LOG_INFO, logFn, "loadThemeStyle: loading for size {}", info.size);
for (auto& shape : impl->theme.shapes) {
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG)
continue; // don't resample NONE style cursors
if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG) {
// don't resample NONE style cursors
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: ignoring {}", shape->directory);
continue;
}
bool sizeFound = false;
@ -327,12 +444,14 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
}
if (!leader) {
Debug::log(ERR, "Resampling failed to find a candidate???");
Debug::log(HC_LOG_ERR, logFn, "Resampling failed to find a candidate???");
return false;
}
const auto FRAMES = impl->getFramesFor(shape.get(), leader->side);
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape {} has {} frames", shape->directory, FRAMES.size());
for (auto& f : FRAMES) {
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true;
@ -343,7 +462,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
const auto PCAIRO = cairo_create(newImage->cairoSurface);
cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE);
cairo_set_antialias(PCAIRO, shape->resizeAlgo == HC_RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE);
cairo_save(PCAIRO);
cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR);
@ -354,7 +473,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE);
const float scale = info.size / (float)f->side;
cairo_scale(PCAIRO, scale, scale);
cairo_pattern_set_filter(PTN, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
cairo_pattern_set_filter(PTN, shape->resizeAlgo == HC_RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
cairo_set_source(PCAIRO, PTN);
cairo_rectangle(PCAIRO, 0, 0, info.size, info.size);
@ -368,6 +487,8 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
} else if (shape->shapeType == SHAPE_SVG) {
const auto FRAMES = impl->getFramesFor(shape.get(), 0);
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape {} has {} frames", shape->directory, FRAMES.size());
for (auto& f : FRAMES) {
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true;
@ -387,14 +508,14 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
RsvgHandle* handle = rsvg_handle_new_from_data((unsigned char*)f->data, f->dataLen, &error);
if (!handle) {
Debug::log(ERR, "Failed reading svg: {}", error->message);
Debug::log(HC_LOG_ERR, logFn, "Failed reading svg: {}", error->message);
return false;
}
RsvgRectangle rect = {0, 0, (double)info.size, (double)info.size};
if (!rsvg_handle_render_document(handle, PCAIRO, &rect, &error)) {
Debug::log(ERR, "Failed rendering svg: {}", error->message);
Debug::log(HC_LOG_ERR, logFn, "Failed rendering svg: {}", error->message);
return false;
}
@ -403,7 +524,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
cairo_destroy(PCAIRO);
}
} else {
Debug::log(ERR, "Invalid shapetype in loadThemeStyle");
Debug::log(HC_LOG_ERR, logFn, "Invalid shapetype in loadThemeStyle");
return false;
}
}
@ -413,7 +534,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
for (auto& shape : impl->theme.shapes) {
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG)
if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG)
continue;
std::erase_if(impl->loadedShapes[shape.get()].images, [info, &shape](const auto& e) {
@ -433,86 +554,8 @@ void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
}
}
/*
Implementation
*/
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
SCursorTheme* currentTheme;
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
image.delay = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
image.filename = RHS;
if (!image.filename.ends_with(".svg")) {
try {
image.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
} else
image.size = 0;
currentTheme->shapes.back()->images.push_back(image);
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
currentTheme->shapes.back()->overrides.push_back(V);
return result;
void CHyprcursorManager::registerLoggingFunction(PHYPRCURSORLOGFUNC fn) {
logFn = fn;
}
/*
@ -524,6 +567,9 @@ PNG reading
static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int len) {
const auto DATA = (SLoadedCursorImage*)data;
if (DATA->readNeedle >= DATA->dataLen)
return CAIRO_STATUS_READ_ERROR;
if (!DATA->data)
return CAIRO_STATUS_READ_ERROR;
@ -532,12 +578,6 @@ static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int le
std::memcpy(output, (uint8_t*)DATA->data + DATA->readNeedle, toRead);
DATA->readNeedle += toRead;
if (DATA->readNeedle >= DATA->dataLen) {
delete[] (char*)DATA->data;
DATA->data = nullptr;
Debug::log(TRACE, "cairo: png read, freeing mem");
}
return CAIRO_STATUS_SUCCESS;
}
@ -552,30 +592,24 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
if (!themeAccessible(themeFullDir))
return "Theme inaccessible";
currentTheme = &theme;
// load manifest
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) {
Debug::log(ERR, "Failed parsing manifest due to {}", err);
return std::string{"failed: "} + err;
}
CManifest manifest(themeFullDir + "/manifest");
const auto PARSERESULT = manifest.parse();
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
if (PARSERESULT.has_value())
return "couldn't parse manifest: " + *PARSERESULT;
const std::string CURSORSSUBDIR = manifest.parsedData.cursorsDirectory;
const std::string CURSORDIR = themeFullDir + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
return "loadTheme: cursors_directory missing or empty";
for (auto& cursor : std::filesystem::directory_iterator(CURSORDIR)) {
if (!cursor.is_regular_file())
if (!cursor.is_regular_file()) {
Debug::log(HC_LOG_TRACE, logFn, "loadTheme: skipping {}", cursor.path().string());
continue;
}
auto& SHAPE = theme.shapes.emplace_back(std::make_unique<SCursorShape>());
auto& LOADEDSHAPE = loadedShapes[SHAPE.get()];
@ -585,8 +619,13 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
zip_t* zip = zip_open(cursor.path().string().c_str(), ZIP_RDONLY, &errp);
zip_file_t* meta_file = zip_fopen(zip, "meta.hl", ZIP_FL_UNCHANGED);
if (!meta_file)
return "cursor" + cursor.path().string() + "failed to load meta";
bool metaIsHL = true;
if (!meta_file) {
meta_file = zip_fopen(zip, "meta.toml", ZIP_FL_UNCHANGED);
metaIsHL = false;
if (!meta_file)
return "cursor" + cursor.path().string() + "failed to load meta";
}
char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */
@ -601,21 +640,20 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
buffer[readBytes] = '\0';
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(buffer, Hyprlang::SConfigOptions{.pathIsStream = true});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
meta->parse();
} catch (const char* err) { return "failed parsing meta: " + std::string{err}; }
CMeta meta{buffer, metaIsHL};
delete[] buffer;
const auto METAPARSERESULT = meta.parse();
if (METAPARSERESULT.has_value())
return "cursor" + cursor.path().string() + "failed to parse meta: " + *METAPARSERESULT;
for (auto& i : meta.parsedData.definedSizes) {
SHAPE->images.push_back(SCursorImage{i.file, i.size, i.delayMs});
}
SHAPE->overrides = meta.parsedData.overrides;
for (auto& i : SHAPE->images) {
if (SHAPE->shapeType == SHAPE_INVALID) {
if (i.filename.ends_with(".svg"))
@ -623,7 +661,7 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
else if (i.filename.ends_with(".png"))
SHAPE->shapeType = SHAPE_PNG;
else {
std::cout << "WARNING: image " << i.filename << " has no known extension, assuming png.\n";
Debug::log(HC_LOG_WARN, logFn, "WARNING: image {} has no known extension, assuming png.", i.filename);
SHAPE->shapeType = SHAPE_PNG;
}
} else {
@ -634,7 +672,7 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
}
// load image
Debug::log(TRACE, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
Debug::log(HC_LOG_TRACE, logFn, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get();
IMAGE->side = SHAPE->shapeType == SHAPE_SVG ? 0 : i.size;
IMAGE->delay = i.delay;
@ -651,19 +689,19 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
zip_fclose(image_file);
Debug::log(TRACE, "Cairo: set up surface read");
Debug::log(HC_LOG_TRACE, logFn, "Cairo: set up surface read");
if (SHAPE->shapeType == SHAPE_PNG) {
IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE);
if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) {
delete[] (char*)IMAGE->data;
delete[](char*) IMAGE->data;
IMAGE->data = nullptr;
return "Failed reading cairoSurface, status " + std::to_string((int)STATUS);
}
} else {
Debug::log(LOG, "Skipping cairo load for a svg surface");
Debug::log(HC_LOG_TRACE, logFn, "Skipping cairo load for a svg surface");
}
}
@ -671,9 +709,9 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
return "meta invalid: no images for shape " + cursor.path().stem().string();
SHAPE->directory = cursor.path().stem().string();
SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
SHAPE->hotspotX = meta.parsedData.hotspotX;
SHAPE->hotspotY = meta.parsedData.hotspotY;
SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo);
zip_discard(zip);
}
@ -695,4 +733,4 @@ std::vector<SLoadedCursorImage*> CHyprcursorImplementation::getFramesFor(SCursor
}
return frames;
}
}

View File

@ -7,6 +7,10 @@ hyprcursor_manager_t* hyprcursor_manager_create(const char* theme_name) {
return (hyprcursor_manager_t*)new CHyprcursorManager(theme_name);
}
hyprcursor_manager_t* hyprcursor_manager_create_with_logger(const char* theme_name, PHYPRCURSORLOGFUNC fn) {
return (hyprcursor_manager_t*)new CHyprcursorManager(theme_name, fn);
}
void hyprcursor_manager_free(hyprcursor_manager_t* manager) {
delete (CHyprcursorManager*)manager;
}
@ -46,4 +50,25 @@ void hyprcursor_style_done(hyprcursor_manager_t* manager, hyprcursor_cursor_styl
SCursorStyleInfo info;
info.size = info_.size;
return MGR->cursorSurfaceStyleDone(info);
}
void hyprcursor_register_logging_function(struct hyprcursor_manager_t* manager, PHYPRCURSORLOGFUNC fn) {
const auto MGR = (CHyprcursorManager*)manager;
MGR->registerLoggingFunction(fn);
}
CAPI hyprcursor_cursor_raw_shape_data* hyprcursor_get_raw_shape_data(struct hyprcursor_manager_t* manager, char* shape) {
const auto MGR = (CHyprcursorManager*)manager;
return MGR->getRawShapeDataC(shape);
}
CAPI void hyprcursor_raw_shape_data_free(hyprcursor_cursor_raw_shape_data* data) {
if (data->overridenBy) {
free(data->overridenBy);
delete data;
return;
}
delete[] data->images;
delete data;
}

View File

@ -18,7 +18,7 @@ struct SLoadedCursorImage {
// read stuff
size_t readNeedle = 0;
void* data = nullptr;
void* data = nullptr; // raw png / svg data, not image data
size_t dataLen = 0;
bool isSVG = false; // if true, data is just a string of chars
@ -37,10 +37,17 @@ struct SLoadedCursorShape {
class CHyprcursorImplementation {
public:
std::string themeName;
std::string themeFullDir;
CHyprcursorImplementation(Hyprcursor::CHyprcursorManager* mgr, PHYPRCURSORLOGFUNC fn) : owner(mgr), logFn(fn) {
;
}
SCursorTheme theme;
Hyprcursor::CHyprcursorManager* owner = nullptr;
PHYPRCURSORLOGFUNC logFn = nullptr;
std::string themeName;
std::string themeFullDir;
SCursorTheme theme;
//
std::unordered_map<SCursorShape*, SLoadedCursorShape> loadedShapes;

View File

@ -2,13 +2,7 @@
#include <string>
#include <vector>
#include <memory>
enum eResizeAlgo {
RESIZE_INVALID = 0,
RESIZE_NONE,
RESIZE_BILINEAR,
RESIZE_NEAREST,
};
#include <hyprcursor/shared.h>
enum eShapeType {
SHAPE_INVALID = 0,
@ -16,19 +10,19 @@ enum eShapeType {
SHAPE_SVG,
};
inline eResizeAlgo stringToAlgo(const std::string& s) {
inline eHyprcursorResizeAlgo stringToAlgo(const std::string& s) {
if (s == "none")
return RESIZE_NONE;
return HC_RESIZE_NONE;
if (s == "nearest")
return RESIZE_NEAREST;
return RESIZE_BILINEAR;
return HC_RESIZE_NEAREST;
return HC_RESIZE_BILINEAR;
}
inline std::string algoToString(const eResizeAlgo a) {
inline std::string algoToString(const eHyprcursorResizeAlgo a) {
switch (a) {
case RESIZE_BILINEAR: return "bilinear";
case RESIZE_NEAREST: return "nearest";
case RESIZE_NONE: return "none";
case HC_RESIZE_BILINEAR: return "bilinear";
case HC_RESIZE_NEAREST: return "nearest";
case HC_RESIZE_NONE: return "none";
default: return "none";
}
@ -44,7 +38,7 @@ struct SCursorImage {
struct SCursorShape {
std::string directory;
float hotspotX = 0, hotspotY = 0;
eResizeAlgo resizeAlgo = RESIZE_NEAREST;
eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NEAREST;
std::vector<SCursorImage> images;
std::vector<std::string> overrides;
eShapeType shapeType = SHAPE_INVALID;

View File

@ -0,0 +1,75 @@
#include "manifest.hpp"
#include <toml++/toml.hpp>
#include <hyprlang.hpp>
#include <filesystem>
CManifest::CManifest(const std::string& path_) {
try {
if (std::filesystem::exists(path_ + ".hl")) {
path = path_ + ".hl";
selectedParser = PARSER_HYPRLANG;
return;
}
if (std::filesystem::exists(path_ + ".toml")) {
path = path_ + ".toml";
selectedParser = PARSER_TOML;
return;
}
} catch (...) { ; }
}
std::optional<std::string> CManifest::parse() {
if (path.empty())
return "Failed to find an appropriate manifest.";
if (selectedParser == PARSER_HYPRLANG)
return parseHL();
if (selectedParser == PARSER_TOML)
return parseTOML();
return "No parser available for " + path;
}
std::optional<std::string> CManifest::parseHL() {
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>(path.c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->addConfigValue("description", Hyprlang::STRING{""});
manifest->addConfigValue("version", Hyprlang::STRING{""});
manifest->addConfigValue("author", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) { return std::string{"failed: "} + err; }
parsedData.cursorsDirectory = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
parsedData.name = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
parsedData.description = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("description"));
parsedData.version = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("version"));
parsedData.author = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("author"));
return {};
}
std::optional<std::string> CManifest::parseTOML() {
try {
auto MANIFEST = toml::parse_file(path);
parsedData.cursorsDirectory = MANIFEST["General"]["cursors_directory"].value_or("");
parsedData.name = MANIFEST["General"]["name"].value_or("");
parsedData.description = MANIFEST["General"]["description"].value_or("");
parsedData.version = MANIFEST["General"]["version"].value_or("");
parsedData.author = MANIFEST["General"]["author"].value_or("");
} catch (...) { return "Failed parsing toml"; }
return {};
}
std::string CManifest::getPath() {
return path;
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <optional>
/*
Manifest can parse manifest.hl and manifest.toml
*/
class CManifest {
public:
/*
path_ is the path to a manifest WITHOUT the extension.
CManifest will attempt all parsable extensions (.hl, .toml)
*/
CManifest(const std::string& path_);
std::optional<std::string> parse();
std::string getPath();
struct {
std::string name, description, version, cursorsDirectory, author;
} parsedData;
private:
enum eParser {
PARSER_HYPRLANG = 0,
PARSER_TOML
};
std::optional<std::string> parseHL();
std::optional<std::string> parseTOML();
eParser selectedParser = PARSER_HYPRLANG;
std::string path;
};

174
libhyprcursor/meta.cpp Normal file
View File

@ -0,0 +1,174 @@
#include "meta.hpp"
#include <hyprlang.hpp>
#include <toml++/toml.hpp>
#include <filesystem>
#include "VarList.hpp"
CMeta* currentMeta = nullptr;
CMeta::CMeta(const std::string& rawdata_, bool hyprlang_ /* false for toml */, bool dataIsPath) : rawdata(rawdata_), hyprlang(hyprlang_), dataPath(dataIsPath) {
if (!dataIsPath)
return;
rawdata = "";
try {
if (std::filesystem::exists(rawdata_ + ".hl")) {
rawdata = rawdata_ + ".hl";
hyprlang = true;
return;
}
if (std::filesystem::exists(rawdata_ + ".toml")) {
rawdata = rawdata_ + ".toml";
hyprlang = false;
return;
}
} catch (...) {}
}
std::optional<std::string> CMeta::parse() {
if (rawdata.empty())
return "Invalid meta (missing?)";
std::optional<std::string> res;
currentMeta = this;
if (hyprlang)
res = parseHL();
else
res = parseTOML();
currentMeta = nullptr;
return res;
}
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
CMeta::SDefinedSize size;
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
size.delayMs = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
size.file = RHS;
if (!size.file.ends_with(".svg")) {
try {
size.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
} else
size.size = 0;
currentMeta->parsedData.definedSizes.push_back(size);
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
currentMeta->parsedData.overrides.push_back(VALUE);
return result;
}
std::optional<std::string> CMeta::parseHL() {
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(rawdata.c_str(), Hyprlang::SConfigOptions{.pathIsStream = !dataPath});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
meta->parse();
} catch (const char* err) { return "failed parsing meta: " + std::string{err}; }
parsedData.hotspotX = std::any_cast<Hyprlang::FLOAT>(meta->getConfigValue("hotspot_x"));
parsedData.hotspotY = std::any_cast<Hyprlang::FLOAT>(meta->getConfigValue("hotspot_y"));
parsedData.resizeAlgo = std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm"));
return {};
}
std::optional<std::string> CMeta::parseTOML() {
try {
auto MANIFEST = dataPath ? toml::parse_file(rawdata) : toml::parse(rawdata);
parsedData.hotspotX = MANIFEST["General"]["hotspot_x"].value_or(0.f);
parsedData.hotspotY = MANIFEST["General"]["hotspot_y"].value_or(0.f);
const std::string OVERRIDES = MANIFEST["General"]["define_override"].value_or("");
const std::string SIZES = MANIFEST["General"]["define_size"].value_or("");
//
CVarList OVERRIDESLIST(OVERRIDES, 0, ';', true);
for (auto& o : OVERRIDESLIST) {
const auto RESULT = ::parseOverride("define_override", o.c_str());
if (RESULT.error)
throw;
}
CVarList SIZESLIST(SIZES, 0, ';', true);
for (auto& s : SIZESLIST) {
const auto RESULT = ::parseDefineSize("define_size", s.c_str());
if (RESULT.error)
throw;
}
parsedData.resizeAlgo = MANIFEST["General"]["resize_algorithm"].value_or("");
} catch (std::exception& e) { return std::string{"Failed parsing toml: "} + e.what(); }
return {};
}

36
libhyprcursor/meta.hpp Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <optional>
#include <vector>
/*
Meta can parse meta.hl and meta.toml
*/
class CMeta {
public:
CMeta(const std::string& rawdata_, bool hyprlang_ /* false for toml */, bool dataIsPath = false);
std::optional<std::string> parse();
struct SDefinedSize {
std::string file;
int size = 0, delayMs = 200;
};
struct {
std::string resizeAlgo;
float hotspotX = 0, hotspotY = 0;
std::vector<std::string> overrides;
std::vector<SDefinedSize> definedSizes;
} parsedData;
private:
std::optional<std::string> parseHL();
std::optional<std::string> parseTOML();
bool dataPath = false;
bool hyprlang = true;
std::string rawdata;
};

View File

@ -7,6 +7,7 @@
hyprlang,
librsvg,
libzip,
tomlplusplus,
version ? "git",
}:
stdenv.mkDerivation {
@ -14,6 +15,11 @@ stdenv.mkDerivation {
inherit version;
src = ../.;
patches = [
# adds /run/current-system/sw/share/icons to the icon lookup directories
./dirs.patch
];
nativeBuildInputs = [
cmake
pkg-config
@ -24,6 +30,7 @@ stdenv.mkDerivation {
hyprlang
librsvg
libzip
tomlplusplus
];
outputs = [

13
nix/dirs.patch Normal file
View File

@ -0,0 +1,13 @@
diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp
index 304ab9f..1f7e95d 100644
--- a/libhyprcursor/hyprcursor.cpp
+++ b/libhyprcursor/hyprcursor.cpp
@@ -14,7 +14,7 @@
using namespace Hyprcursor;
// directories for lookup
-constexpr const std::array<const char*, 1> systemThemeDirs = {"/usr/share/icons"};
+constexpr const std::array<const char*, 2> systemThemeDirs = {"/usr/share/icons", "/run/current-system/sw/share/icons"};
constexpr const std::array<const char*, 2> userThemeDirs = {"/.local/share/icons", "/.icons"};
//

View File

@ -1,19 +1,46 @@
/*
hyprlang-test in C.
Renders a cursor shape to /tmp at 48px
For better explanations, see the cpp tests.
*/
#include <hyprcursor/hyprcursor.h>
#include <stdio.h>
#include <stdlib.h>
void logFunction(enum eHyprcursorLogLevel level, char* message) {
printf("[hc] %s\n", message);
}
int main(int argc, char** argv) {
struct hyprcursor_manager_t* mgr = hyprcursor_manager_create(NULL);
struct hyprcursor_manager_t* mgr = hyprcursor_manager_create_with_logger(NULL, logFunction);
if (!mgr) {
printf("mgr null\n");
return 1;
}
if (!hyprcursor_manager_valid(mgr)) {
printf("mgr is invalid\n");
return 1;
}
hyprcursor_cursor_raw_shape_data* shapeData = hyprcursor_get_raw_shape_data(mgr, "left_ptr");
if (!shapeData || shapeData->len <= 0) {
printf("failed querying left_ptr\n");
return 1;
}
printf("left_ptr images: %d\n", shapeData->len);
for (size_t i = 0; i < shapeData->len; ++i) {
printf("left_ptr image size: %d\n", shapeData->images[i].len);
}
hyprcursor_raw_shape_data_free(shapeData);
shapeData = NULL;
struct hyprcursor_cursor_style_info info = {.size = 48};
if (!hyprcursor_load_theme_style(mgr, info)) {
printf("load failed\n");

76
tests/full_rendering.cpp Normal file
View File

@ -0,0 +1,76 @@
/*
full_rendering.cpp
This example shows probably what you want to do.
Hyprcursor will render a left_ptr shape at 48x48px to a file called /tmp/arrow.png
*/
#include <iostream>
#include <hyprcursor/hyprcursor.hpp>
void logFunction(enum eHyprcursorLogLevel level, char* message) {
std::cout << "[hc] " << message << "\n";
}
int main(int argc, char** argv) {
/*
Create a manager. You can optionally pass a logger function.
*/
Hyprcursor::CHyprcursorManager mgr(nullptr, logFunction);
/*
Manager could be invalid if no themes were found, or
a specified theme was invalid.
*/
if (!mgr.valid()) {
std::cout << "mgr is invalid\n";
return 1;
}
/*
Style describes what pixel size you want your cursor
images to be.
Remember to free styles once you're done with them
(e.g. the user requested to change the cursor size to something else)
*/
Hyprcursor::SCursorStyleInfo style{.size = 48};
if (!mgr.loadThemeStyle(style)) {
std::cout << "failed loading style\n";
return 1;
}
/*
Get a shape. This will return the data about available image(s),
their delay, hotspot, etc.
*/
const auto SHAPEDATA = mgr.getShape("left_ptr", style);
/*
If the size doesn't exist, images will be empty.
*/
if (SHAPEDATA.images.empty()) {
std::cout << "no images\n";
return 1;
}
std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n";
/*
Save to disk with cairo
*/
const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png");
std::cout << "Cairo returned for write: " << RET << "\n";
/*
As mentioned before, clean up by releasing the style.
*/
mgr.cursorSurfaceStyleDone(style);
if (RET)
return 1;
return !mgr.valid();
}

80
tests/only_metadata.cpp Normal file
View File

@ -0,0 +1,80 @@
/*
only_metadata.cpp
This is a mode in which you probably do NOT want to operate,
but major DEs might want their own renderer for
cursor shapes.
Prefer full_rendering.cpp for consistency and simplicity.
*/
#include <iostream>
#include <hyprcursor/hyprcursor.hpp>
void logFunction(enum eHyprcursorLogLevel level, char* message) {
std::cout << "[hc] " << message << "\n";
}
int main(int argc, char** argv) {
/*
Create a manager. You can optionally pass a logger function.
*/
Hyprcursor::CHyprcursorManager mgr(nullptr, logFunction);
/*
Manager could be invalid if no themes were found, or
a specified theme was invalid.
*/
if (!mgr.valid()) {
std::cout << "mgr is invalid\n";
return 1;
}
/*
If you are planning on using your own renderer,
you do not want to load in any styles, as those
are rendered once you make your call.
Instead, let's request left_ptr's metadata
*/
auto RAWDATA = mgr.getRawShapeData("left_ptr");
/*
if images are empty, check overridenBy
*/
if (RAWDATA.images.empty()) {
/*
if overridenBy is empty, the current theme doesn't have this shape.
*/
if (RAWDATA.overridenBy.empty())
return false;
/*
load what it's overriden by.
*/
RAWDATA = mgr.getRawShapeData(RAWDATA.overridenBy.c_str());
}
/*
If we still have no images, the theme seems broken.
*/
if (RAWDATA.images.empty()) {
std::cout << "failed querying left_ptr\n";
return 1;
}
/*
You can query the images (animation frames)
or their properties.
Every image has .data and .type for you to handle.
*/
std::cout << "left_ptr images: " << RAWDATA.images.size() << "\n";
for (auto& i : RAWDATA.images)
std::cout << "left_ptr data size: " << i.data.size() << "\n";
return 0;
}

View File

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