Initial commit: very basics

This commit is contained in:
Vaxry 2024-03-07 03:19:38 +00:00
parent 47d8cfc1c8
commit 5227dcc787
15 changed files with 1072 additions and 0 deletions

65
.clang-format Normal file
View file

@ -0,0 +1,65 @@
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: true
AlignConsecutiveAssignments: true
AlignEscapedNewlines: Right
AlignOperands: false
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: false
BreakConstructorInitializers: AfterColon
ColumnLimit: 180
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
IncludeBlocks: Preserve
IndentCaseLabels: true
IndentWidth: 4
PointerAlignment: Left
ReflowComments: false
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
TabWidth: 4
UseTab: Never
AllowShortEnumsOnASingleLine: false
BraceWrapping:
AfterEnum: false
AlignConsecutiveDeclarations: AcrossEmptyLines
NamespaceIndentation: All

2
.gitignore vendored Normal file
View file

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

71
CMakeLists.txt Normal file
View file

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

View file

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

View file

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

View file

@ -0,0 +1,246 @@
#include <iostream>
#include <zip.h>
#include <optional>
#include <filesystem>
#include <hyprlang.hpp>
#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<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;
}
std::optional<std::string> 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<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(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<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
const std::string CURSORDIR = path + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
return "manifest: cursors_directory missing or empty";
// iterate over the directory and record all cursors
currentTheme = std::make_unique<SCursorTheme>();
for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) {
const auto METAPATH = dir.path().string() + "/meta.hl";
auto& SHAPE = currentTheme->shapes.emplace_back();
//
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(METAPATH.c_str(), Hyprlang::SConfigOptions{});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
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<float>(meta->getConfigValue("hotspot_x"));
SHAPE.hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE.resizeAlgo = std::string{std::any_cast<Hyprlang::STRING>(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;
}

10
hyprcursor.pc.in Normal file
View file

@ -0,0 +1,10 @@
prefix=@PREFIX@
includedir=@INCLUDE@
libdir=@LIBDIR@
Name: hyprcursor
URL: https://github.com/hyprwm/hyprcursor
Description: A library and toolkit for the Hyprland cursor format
Version: @HYPRCURSOR_VERSION@
Cflags: -I${includedir}
Libs: -L${libdir} -lhyprcursor

44
include/hyprcursor.h Normal file
View file

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

62
include/hyprcursor.hpp Normal file
View file

@ -0,0 +1,62 @@
#pragma once
#include <cairo/cairo.h>
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;
};
}

53
libhyprcursor/Log.hpp Normal file
View file

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

View file

@ -0,0 +1,392 @@
#include "hyprcursor.hpp"
#include "internalSharedTypes.hpp"
#include "internalDefines.hpp"
#include <array>
#include <filesystem>
#include <hyprlang.hpp>
#include <zip.h>
#include <cstring>
#include <algorithm>
#include "Log.hpp"
using namespace Hyprcursor;
// directories for lookup
constexpr const std::array<const char*, 1> systemThemeDirs = {"/usr/share/icons"};
constexpr const std::array<const char*, 2> userThemeDirs = {"/.local/share/icons", "/.icons"};
//
static std::string themeNameFromEnv() {
const auto ENV = getenv("HYPRCURSOR_THEME");
if (!ENV)
return "";
return std::string{ENV};
}
static 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<std::string> CHyprcursorImplementation::loadTheme() {
currentTheme = &theme;
// load manifest
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) {
Debug::log(ERR, "Failed parsing manifest due to {}", err);
return std::string{"failed: "} + err;
}
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
const std::string CURSORDIR = themeFullDir + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
return "loadTheme: cursors_directory missing or empty";
for (auto& cursor : std::filesystem::directory_iterator(CURSORDIR)) {
if (!cursor.is_regular_file())
continue;
auto& SHAPE = theme.shapes.emplace_back();
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<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(buffer, Hyprlang::SConfigOptions{.pathIsStream = true});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
meta->parse();
} catch (const char* err) { return "failed parsing meta: " + std::string{err}; }
delete[] buffer;
for (auto& i : SHAPE.images) {
// load image
Debug::log(LOG, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).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<float>(meta->getConfigValue("hotspot_x"));
SHAPE.hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE.resizeAlgo = std::string{std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm"))} == "nearest" ? RESIZE_NEAREST : RESIZE_BILINEAR;
zip_discard(zip);
}
return {};
}

View file

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

View file

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

View file

@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <vector>
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<SCursorImage> images;
std::vector<std::string> overrides;
};
struct SCursorTheme {
std::vector<SCursorShape> shapes;
};

17
tests/test.cpp Normal file
View file

@ -0,0 +1,17 @@
#include <iostream>
#include <hyprcursor.hpp>
#include <cairo/cairo.h>
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();
}