From 5227dcc78765722d177bfb7f30ccf118c2aa813b Mon Sep 17 00:00:00 2001 From: Vaxry Date: Thu, 7 Mar 2024 03:19:38 +0000 Subject: [PATCH] Initial commit: very basics --- .clang-format | 65 ++++ .gitignore | 2 + CMakeLists.txt | 71 +++++ hyprcursor-util/CMakeLists.txt | 23 ++ hyprcursor-util/internalSharedTypes.hpp | 1 + hyprcursor-util/src/main.cpp | 246 +++++++++++++++ hyprcursor.pc.in | 10 + include/hyprcursor.h | 44 +++ include/hyprcursor.hpp | 62 ++++ libhyprcursor/Log.hpp | 53 ++++ libhyprcursor/hyprcursor.cpp | 392 ++++++++++++++++++++++++ libhyprcursor/hyprcursor_c.cpp | 17 + libhyprcursor/internalDefines.hpp | 40 +++ libhyprcursor/internalSharedTypes.hpp | 29 ++ tests/test.cpp | 17 + 15 files changed, 1072 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 hyprcursor-util/CMakeLists.txt create mode 120000 hyprcursor-util/internalSharedTypes.hpp create mode 100644 hyprcursor-util/src/main.cpp create mode 100644 hyprcursor.pc.in create mode 100644 include/hyprcursor.h create mode 100644 include/hyprcursor.hpp create mode 100644 libhyprcursor/Log.hpp create mode 100644 libhyprcursor/hyprcursor.cpp create mode 100644 libhyprcursor/hyprcursor_c.cpp create mode 100644 libhyprcursor/internalDefines.hpp create mode 100644 libhyprcursor/internalSharedTypes.hpp create mode 100644 tests/test.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +ColumnLimit: 180 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentWidth: 4 +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never + +AllowShortEnumsOnASingleLine: false + +BraceWrapping: + AfterEnum: false + +AlignConsecutiveDeclarations: AcrossEmptyLines + +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e524d79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d661b89 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.19) + +set(HYPRCURSOR_VERSION "0.1.0") + +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) + +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.hpp" "include/hyprcursor.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.hpp + PUBLIC_HEADER include/hyprcursor.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" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test) +add_dependencies(tests hyprcursor_test) + +# Installation +install(TARGETS hyprcursor + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) diff --git a/hyprcursor-util/CMakeLists.txt b/hyprcursor-util/CMakeLists.txt new file mode 100644 index 0000000..329be9c --- /dev/null +++ b/hyprcursor-util/CMakeLists.txt @@ -0,0 +1,23 @@ +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) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") + +set(CMAKE_CXX_STANDARD 23) + +add_executable(hyprcursor-util ${SRCFILES}) + +target_link_libraries(hyprcursor-util PkgConfig::deps) +target_include_directories(hyprcursor-util +PRIVATE + . +) + +install(TARGETS hyprcursor-util) \ No newline at end of file diff --git a/hyprcursor-util/internalSharedTypes.hpp b/hyprcursor-util/internalSharedTypes.hpp new file mode 120000 index 0000000..9402209 --- /dev/null +++ b/hyprcursor-util/internalSharedTypes.hpp @@ -0,0 +1 @@ +../libhyprcursor/internalSharedTypes.hpp \ No newline at end of file diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp new file mode 100644 index 0000000..c386a46 --- /dev/null +++ b/hyprcursor-util/src/main.cpp @@ -0,0 +1,246 @@ +#include +#include +#include +#include +#include +#include "internalSharedTypes.hpp" + +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; +} + +std::unique_ptr currentTheme; + +static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { + Hyprlang::CParseResult result; + const std::string VALUE = V; + + if (!VALUE.contains(",")) { + result.setError("Invalid define_size"); + return result; + } + + const auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); + const auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + + SCursorImage image; + 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; +} + +std::optional createCursorThemeFromPath(const std::string& path, const std::string& out_ = {}) { + if (!std::filesystem::exists(path)) + return "input path does not exist"; + + std::string out = out_.empty() ? path.substr(0, path.find_last_of('/') + 1) + "theme/" : out_; + + const auto MANIFESTPATH = path + "/manifest.hl"; + if (!std::filesystem::exists(MANIFESTPATH)) + return "manifest.hl is missing"; + + std::unique_ptr manifest; + try { + manifest = std::make_unique(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{}); + manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""}); + manifest->commence(); + manifest->parse(); + } catch (const char* err) { return "failed parsing manifest: " + std::string{err}; } + + const std::string CURSORSSUBDIR = std::any_cast(manifest->getConfigValue("cursors_directory")); + const std::string CURSORDIR = path + "/" + CURSORSSUBDIR; + + if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR)) + return "manifest: cursors_directory missing or empty"; + + // iterate over the directory and record all cursors + + currentTheme = std::make_unique(); + for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) { + const auto METAPATH = dir.path().string() + "/meta.hl"; + + auto& SHAPE = currentTheme->shapes.emplace_back(); + + // + std::unique_ptr meta; + + try { + meta = std::make_unique(METAPATH.c_str(), Hyprlang::SConfigOptions{}); + meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F}); + meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F}); + meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"}); + meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false}); + meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false}); + meta->commence(); + meta->parse(); + } 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 (!std::filesystem::exists(dir.path().string() + "/" + i.filename)) + return "meta invalid: image " + i.filename + " does not exist"; + break; + } + + if (SHAPE.images.empty()) + return "meta invalid: no images for shape " + dir.path().stem().string(); + + SHAPE.directory = dir.path().stem().string(); + SHAPE.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + SHAPE.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + SHAPE.resizeAlgo = std::string{std::any_cast(meta->getConfigValue("resize_algorithm"))} == "nearest" ? RESIZE_NEAREST : RESIZE_BILINEAR; + + 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 + std::filesystem::remove_all(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 {}; +} + +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 (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 { + std::cerr << "Invalid mode.\n"; + return 1; + } + continue; + } + + if (arg == "-o" || arg == "--output") { + out = argv[++i]; + continue; + } else { + std::cerr << "Unknown arg: " << arg << "\n"; + return 1; + } + } + + switch (op) { + case OPERATION_CREATE: { + const auto RET = createCursorThemeFromPath(path, out); + if (RET.has_value()) { + std::cerr << "Failed: " << RET.value() << "\n"; + return 1; + } + break; + } + default: std::cerr << "Invalid mode.\n"; return 1; + } + + return 0; +} \ No newline at end of file diff --git a/hyprcursor.pc.in b/hyprcursor.pc.in new file mode 100644 index 0000000..35e9a52 --- /dev/null +++ b/hyprcursor.pc.in @@ -0,0 +1,10 @@ +prefix=@PREFIX@ +includedir=@INCLUDE@ +libdir=@LIBDIR@ + +Name: hyprcursor +URL: https://github.com/hyprwm/hyprcursor +Description: A library and toolkit for the Hyprland cursor format +Version: @HYPRCURSOR_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -lhyprcursor diff --git a/include/hyprcursor.h b/include/hyprcursor.h new file mode 100644 index 0000000..1efaec1 --- /dev/null +++ b/include/hyprcursor.h @@ -0,0 +1,44 @@ + +#ifndef HYPRCURSOR_H +#define HYPRCURSOR_H + +#ifdef __cplusplus + +#define CAPI extern "C" + +#else + +#define CAPI + +#endif + +struct hyprcursor_manager_t; + +/*! + 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 hyprcursor_manager_t* hyprcursor_manager_create(const char* theme_name); + +/*! + Free a hyprcursor_manager_t* +*/ +CAPI void hyprcursor_manager_free(hyprcursor_manager_t* manager); + +/*! + Returns true if the theme was successfully loaded, + i.e. everything is A-OK and nothing should fail. +*/ +CAPI bool hyprcursor_manager_valid(hyprcursor_manager_t* manager); + +#endif \ No newline at end of file diff --git a/include/hyprcursor.hpp b/include/hyprcursor.hpp new file mode 100644 index 0000000..281070a --- /dev/null +++ b/include/hyprcursor.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +class CHyprcursorImplementation; + +namespace Hyprcursor { + + /*! + Simple struct for some info about shape requests + */ + struct SCursorSurfaceInfo { + /* + Shape size + */ + unsigned int size = 0; + }; + + /*! + 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(); + + /*! + Returns a cairo_surface_t for a given cursor + shape and size. + + Once done, call cursorSurfaceDone() + */ + cairo_surface_t* getSurfaceFor(const char* shape, const SCursorSurfaceInfo& info); + + /*! + Marks a surface as done, meaning ready to be freed. + + Always call after using a surface. + */ + void cursorSurfaceDone(cairo_surface_t* surface); + + private: + CHyprcursorImplementation* impl = nullptr; + bool finalizedAndValid = false; + }; + +} \ No newline at end of file diff --git a/libhyprcursor/Log.hpp b/libhyprcursor/Log.hpp new file mode 100644 index 0000000..75ad948 --- /dev/null +++ b/libhyprcursor/Log.hpp @@ -0,0 +1,53 @@ +#pragma once + +enum eLogLevel { + TRACE = 0, + INFO, + LOG, + WARN, + ERR, + CRIT, + NONE +}; + +#include +#include +#include + +namespace Debug { + inline bool quiet = false; + inline bool verbose = false; + + template + void log(eLogLevel level, const std::string& fmt, Args&&... args) { + +#ifndef HYPRLAND_DEBUG + // don't log in release + return; +#endif + + if (!verbose && level == TRACE) + return; + + if (quiet) + return; + + if (level != NONE) { + std::cout << '['; + + switch (level) { + case TRACE: std::cout << "TRACE"; break; + case INFO: std::cout << "INFO"; break; + case LOG: std::cout << "LOG"; break; + case WARN: std::cout << "WARN"; break; + case ERR: std::cout << "ERR"; break; + case CRIT: std::cout << "CRITICAL"; break; + default: break; + } + + std::cout << "] "; + } + + std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n"; + } +}; \ No newline at end of file diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp new file mode 100644 index 0000000..747b17b --- /dev/null +++ b/libhyprcursor/hyprcursor.cpp @@ -0,0 +1,392 @@ +#include "hyprcursor.hpp" +#include "internalSharedTypes.hpp" +#include "internalDefines.hpp" +#include +#include +#include +#include +#include +#include + +#include "Log.hpp" + +using namespace Hyprcursor; + +// directories for lookup +constexpr const std::array systemThemeDirs = {"/usr/share/icons"}; +constexpr const std::array userThemeDirs = {"/.local/share/icons", "/.icons"}; + +// +static std::string themeNameFromEnv() { + const auto ENV = getenv("HYPRCURSOR_THEME"); + if (!ENV) + return ""; + + return std::string{ENV}; +} + +static 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; + + 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; + + const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl"; + + if (std::filesystem::exists(MANIFESTPATH)) + return std::filesystem::canonical(themeDir.path()).string(); + } + } + + 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; +} + +cairo_surface_t* CHyprcursorManager::getSurfaceFor(const char* shape_, const SCursorSurfaceInfo& info) { + std::string REQUESTEDSHAPE = shape_; + + for (auto& shape : impl->theme.shapes) { + if (REQUESTEDSHAPE != shape.directory && std::find(shape.overrides.begin(), shape.overrides.end(), REQUESTEDSHAPE) == shape.overrides.end()) + continue; + + // matched :) + for (auto& image : impl->loadedShapes[&shape].images) { + if (image->side != info.size) + continue; + + // found pixel-perfect size + return image->cairoSurface; + } + + // TODO: resampling + } + return nullptr; +} + +void CHyprcursorManager::cursorSurfaceDone(cairo_surface_t* surface) { + ; + // TODO: when resampling. +} + +/* + +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; + } + + const auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); + const auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + + SCursorImage image; + 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(LOG, "cairo: png read, freeing mem"); + } + + return CAIRO_STATUS_SUCCESS; +} + +/* + +General + +*/ + +std::optional CHyprcursorImplementation::loadTheme() { + + currentTheme = &theme; + + // load manifest + std::unique_ptr manifest; + try { + // TODO: unify this between util and lib + manifest = std::make_unique((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{}); + manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""}); + manifest->commence(); + manifest->parse(); + } catch (const char* err) { + Debug::log(ERR, "Failed parsing manifest due to {}", err); + return std::string{"failed: "} + err; + } + + const std::string CURSORSSUBDIR = std::any_cast(manifest->getConfigValue("cursors_directory")); + const std::string CURSORDIR = themeFullDir + "/" + CURSORSSUBDIR; + + if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR)) + return "loadTheme: cursors_directory missing or empty"; + + for (auto& cursor : std::filesystem::directory_iterator(CURSORDIR)) { + if (!cursor.is_regular_file()) + continue; + + auto& SHAPE = theme.shapes.emplace_back(); + auto& LOADEDSHAPE = loadedShapes[&SHAPE]; + + // extract zip to raw data. + int errp = 0; + zip_t* zip = zip_open(cursor.path().string().c_str(), ZIP_RDONLY, &errp); + + zip_file_t* meta_file = zip_fopen(zip, "meta.hl", ZIP_FL_UNCHANGED); + if (!meta_file) + return "cursor" + cursor.path().string() + "failed to load meta"; + + char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */ + + int readBytes = zip_fread(meta_file, buffer, 1024 * 1024 - 1); + + zip_fclose(meta_file); + + if (readBytes < 0) { + delete[] buffer; + return "cursor" + cursor.path().string() + "failed to read meta"; + } + + buffer[readBytes] = '\0'; + + std::unique_ptr meta; + + try { + meta = std::make_unique(buffer, Hyprlang::SConfigOptions{.pathIsStream = true}); + meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F}); + meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F}); + meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"}); + meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false}); + meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false}); + meta->commence(); + meta->parse(); + } catch (const char* err) { return "failed parsing meta: " + std::string{err}; } + + delete[] buffer; + + for (auto& i : SHAPE.images) { + // load image + Debug::log(LOG, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); + auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique()).get(); + IMAGE->side = i.size; + + // 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(LOG, "Cairo: set up surface read"); + + 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); + } + } + + if (SHAPE.images.empty()) + return "meta invalid: no images for shape " + cursor.path().stem().string(); + + SHAPE.directory = cursor.path().stem().string(); + SHAPE.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + SHAPE.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + SHAPE.resizeAlgo = std::string{std::any_cast(meta->getConfigValue("resize_algorithm"))} == "nearest" ? RESIZE_NEAREST : RESIZE_BILINEAR; + + zip_discard(zip); + } + + return {}; +} \ No newline at end of file diff --git a/libhyprcursor/hyprcursor_c.cpp b/libhyprcursor/hyprcursor_c.cpp new file mode 100644 index 0000000..a5ba8f8 --- /dev/null +++ b/libhyprcursor/hyprcursor_c.cpp @@ -0,0 +1,17 @@ +#include "hyprcursor.h" +#include "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; +} + +bool hyprcursor_manager_valid(hyprcursor_manager_t* manager) { + const auto MGR = (CHyprcursorManager*)manager; + return MGR->valid(); +} \ No newline at end of file diff --git a/libhyprcursor/internalDefines.hpp b/libhyprcursor/internalDefines.hpp new file mode 100644 index 0000000..6124688 --- /dev/null +++ b/libhyprcursor/internalDefines.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "internalSharedTypes.hpp" +#include +#include +#include +#include + +struct SLoadedCursorImage { + ~SLoadedCursorImage() { + if (data) + delete[] (char*)data; + } + + // read stuff + size_t readNeedle = 0; + void* data = nullptr; + size_t dataLen = 0; + + cairo_surface_t* cairoSurface = nullptr; + int side = 0; +}; + +struct SLoadedCursorShape { + std::vector> images; +}; + +class CHyprcursorImplementation { + public: + std::string themeName; + std::string themeFullDir; + + SCursorTheme theme; + + // + std::unordered_map loadedShapes; + + // + std::optional loadTheme(); +}; \ No newline at end of file diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp new file mode 100644 index 0000000..9c3cce2 --- /dev/null +++ b/libhyprcursor/internalSharedTypes.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +enum eOperation { + OPERATION_CREATE = 0, +}; + +enum eResizeAlgo { + RESIZE_BILINEAR = 0, + RESIZE_NEAREST = 1, +}; + +struct SCursorImage { + std::string filename; + int size = 0; +}; + +struct SCursorShape { + std::string directory; + float hotspotX = 0, hotspotY = 0; + eResizeAlgo resizeAlgo = RESIZE_NEAREST; + std::vector images; + std::vector overrides; +}; + +struct SCursorTheme { + std::vector shapes; +}; \ No newline at end of file diff --git a/tests/test.cpp b/tests/test.cpp new file mode 100644 index 0000000..7479b9e --- /dev/null +++ b/tests/test.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +int main(int argc, char** argv) { + Hyprcursor::CHyprcursorManager mgr(nullptr); + + // get cursor for arrow + const auto ARROW = mgr.getSurfaceFor("arrow", Hyprcursor::SCursorSurfaceInfo{.size = 64}); + + // save to disk + const auto RET = cairo_surface_write_to_png(ARROW, "/tmp/arrow.png"); + + std::cout << "Cairo returned for write: " << RET << "\n"; + + return !mgr.valid(); +} \ No newline at end of file