#include "hyprcursor/hyprcursor.hpp" #include "internalSharedTypes.hpp" #include "internalDefines.hpp" #include #include #include #include #include #include #include #include #include #include "manifest.hpp" #include "meta.hpp" #include "Log.hpp" using namespace Hyprcursor; static std::vector getSystemThemeDirs() { const auto envXdgData = std::getenv("XDG_DATA_DIRS"); std::vector result; if (envXdgData) { std::stringstream envXdgStream(envXdgData); std::string tmpStr; while (getline(envXdgStream, tmpStr, ':')) result.push_back((tmpStr + "/icons")); } else result = {"/usr/share/icons"}; return result; } const std::vector systemThemeDirs = getSystemThemeDirs(); constexpr const std::array userThemeDirs = {"/.local/share/icons", "/.icons"}; // static std::string themeNameFromEnv(PHYPRCURSORLOGFUNC logfn) { const auto ENV = getenv("HYPRCURSOR_THEME"); if (!ENV) { Debug::log(HC_LOG_INFO, logfn, "themeNameFromEnv: env unset"); return ""; } return std::string{ENV}; } static bool pathAccessible(const std::string& path) { try { if (!std::filesystem::exists(path)) return false; } catch (std::exception& e) { return false; } return true; } 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"); if (!HOMEENV) return ""; const std::string HOME{HOMEENV}; for (auto& dir : userThemeDirs) { const auto FULLPATH = HOME + dir; 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 (!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."; 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 (!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 (!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."; 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, PHYPRCURSORLOGFUNC logfn, bool allowDefaultFallback) { const auto HOMEENV = getenv("HOME"); if (!HOMEENV) return ""; const std::string HOME{HOMEENV}; for (auto& dir : userThemeDirs) { const auto FULLPATH = HOME + dir; 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 (!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"; if (allowDefaultFallback && 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 (!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 (!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; Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string()); return std::filesystem::canonical(themeDir.path()).string(); } } if (allowDefaultFallback && !name.empty()) { // try without name Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: failed, trying without name of {}", name); return getFullPathForThemeName("", logfn, allowDefaultFallback); } return ""; } SManagerOptions::SManagerOptions() { logFn = nullptr; allowDefaultFallback = true; } CHyprcursorManager::CHyprcursorManager(const char* themeName_) { init(themeName_); } CHyprcursorManager::CHyprcursorManager(const char* themeName_, PHYPRCURSORLOGFUNC fn) { logFn = fn; init(themeName_); } CHyprcursorManager::CHyprcursorManager(const char* themeName_, SManagerOptions options) { logFn = options.logFn; allowDefaultFallback = options.allowDefaultFallback; init(themeName_); } void CHyprcursorManager::init(const char* themeName_) { std::string themeName = themeName_ ? themeName_ : ""; if (allowDefaultFallback && themeName.empty()) { // try reading from env Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find theme from env"); themeName = themeNameFromEnv(logFn); } if (allowDefaultFallback && themeName.empty()) { // try finding first, in the hierarchy 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(this, logFn); impl->themeName = themeName; impl->themeFullDir = getFullPathForThemeName(themeName, logFn, allowDefaultFallback); if (impl->themeFullDir.empty()) return; 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(HC_LOG_ERR, logFn, "Theme failed to load with {}\n", LOADSTATUS.value()); return; } if (impl->theme.shapes.empty()) { Debug::log(HC_LOG_ERR, logFn, "Theme {} has no valid cursor shapes\n", impl->themeName); return; } finalizedAndValid = true; } CHyprcursorManager::~CHyprcursorManager() { if (impl) delete impl; } bool CHyprcursorManager::valid() { return finalizedAndValid; } SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info) { if (!shape_) { Debug::log(HC_LOG_ERR, logFn, "getShapesC: shape of nullptr is invalid"); return nullptr; } std::string REQUESTEDSHAPE = shape_; std::vector resultingImages; float hotX = 0, hotY = 0; for (auto& shape : impl->theme.shapes) { if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end()) continue; const int PIXELSIDE = std::round(info.size / shape->nominalSize); hotX = shape->hotspotX; hotY = shape->hotspotY; // matched :) bool foundAny = false; for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != PIXELSIDE) continue; // found size resultingImages.push_back(image.get()); foundAny = true; } if (foundAny || shape->shapeType == SHAPE_SVG /* something broke, this shouldn't happen with svg */) break; // if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error. if (shape->resizeAlgo != HC_RESIZE_NONE) { Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match a size?"); return nullptr; } // find nearest int leader = 13371337; for (auto& image : impl->loadedShapes[shape.get()].images) { if (std::abs((int)(image->side - PIXELSIDE)) > std::abs((int)(leader - PIXELSIDE))) continue; leader = image->side; } if (leader == 13371337) { // ??? Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size?"); return nullptr; } // we found nearest size for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != leader) continue; // found size resultingImages.push_back(image.get()); foundAny = true; } if (foundAny) break; Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size (2)?"); return nullptr; } // alloc and return what we need SCursorImageData** data = (SCursorImageData**)malloc(sizeof(SCursorImageData*) * resultingImages.size()); for (size_t i = 0; i < resultingImages.size(); ++i) { data[i] = (SCursorImageData*)malloc(sizeof(SCursorImageData)); data[i]->delay = resultingImages[i]->delay; data[i]->size = resultingImages[i]->side; data[i]->surface = resultingImages[i]->cairoSurface; data[i]->hotspotX = 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 resultingImages; data->overridenBy = nullptr; data->images = nullptr; data->len = 0; data->hotspotX = 0.f; data->hotspotY = 0.F; data->nominalSize = 1.F; data->resizeAlgo = eHyprcursorResizeAlgo::HC_RESIZE_NONE; data->type = eHyprcursorDataType::HC_DATA_PNG; for (auto& shape : impl->theme.shapes) { // if it's overridden just return the override if (const auto IT = std::find_if(shape->overrides.begin(), shape->overrides.end(), [&](const auto& e) { return e == SHAPE && SHAPE != shape->directory; }); IT != shape->overrides.end()) { data->overridenBy = strdup(shape->directory.c_str()); return data; } } for (auto& shape : impl->theme.shapes) { 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->nominalSize = shape->nominalSize; 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 == 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; if (shape->shapeType == SHAPE_PNG) { for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != std::round(info.size / shape->nominalSize)) continue; sizeFound = true; break; } // size wasn't found, let's resample. if (sizeFound) continue; SLoadedCursorImage* leader = nullptr; int leaderVal = 1000000; for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side < info.size) continue; if (image->side > leaderVal) continue; leaderVal = image->side; leader = image.get(); } if (!leader) { for (auto& image : impl->loadedShapes[shape.get()].images) { if (std::abs((int)(image->side - info.size)) > leaderVal) continue; leaderVal = image->side; leader = image.get(); } } if (!leader) { Debug::log(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()); const int PIXELSIDE = std::round(info.size / shape->nominalSize); Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape has nominal {:.2f}, pixel size will be {}x", shape->nominalSize, PIXELSIDE); for (auto& f : FRAMES) { auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); newImage->artificial = true; newImage->side = PIXELSIDE; newImage->artificialData = new char[PIXELSIDE * PIXELSIDE * 4]; newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, PIXELSIDE, PIXELSIDE, PIXELSIDE * 4); newImage->delay = f->delay; const auto PCAIRO = cairo_create(newImage->cairoSurface); 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); cairo_paint(PCAIRO); cairo_restore(PCAIRO); const auto PTN = cairo_pattern_create_for_surface(f->cairoSurface); cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); const float scale = PIXELSIDE / (float)f->side; cairo_scale(PCAIRO, scale, scale); 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, PIXELSIDE, PIXELSIDE); cairo_fill(PCAIRO); cairo_surface_flush(newImage->cairoSurface); cairo_pattern_destroy(PTN); cairo_destroy(PCAIRO); } } else if (shape->shapeType == SHAPE_SVG) { const auto FRAMES = impl->getFramesFor(shape.get(), 0); Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape {} has {} frames", shape->directory, FRAMES.size()); const int PIXELSIDE = std::round(info.size / shape->nominalSize); Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape has nominal {:.2f}, pixel size will be {}x", shape->nominalSize, PIXELSIDE); for (auto& f : FRAMES) { auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); newImage->artificial = true; newImage->side = PIXELSIDE; newImage->artificialData = new char[PIXELSIDE * PIXELSIDE * 4]; newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, PIXELSIDE, PIXELSIDE, PIXELSIDE * 4); newImage->delay = f->delay; const auto PCAIRO = cairo_create(newImage->cairoSurface); cairo_save(PCAIRO); cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); cairo_paint(PCAIRO); cairo_restore(PCAIRO); GError* error = nullptr; RsvgHandle* handle = rsvg_handle_new_from_data((unsigned char*)f->data, f->dataLen, &error); if (!handle) { Debug::log(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(HC_LOG_ERR, logFn, "Failed rendering svg: {}", error->message); return false; } // done cairo_surface_flush(newImage->cairoSurface); cairo_destroy(PCAIRO); } } else { Debug::log(HC_LOG_ERR, logFn, "Invalid shapetype in loadThemeStyle"); return false; } } return true; } void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { for (auto& shape : impl->theme.shapes) { if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG) continue; std::erase_if(impl->loadedShapes[shape.get()].images, [info, &shape](const auto& e) { const bool isSVG = shape->shapeType == SHAPE_SVG; const bool isArtificial = e->artificial; // clean artificial rasters made for this if (isArtificial && e->side == std::round(info.size / shape->nominalSize)) return true; // clean invalid non-svg rasters if (!isSVG && e->side == 0) return true; return false; }); } } void CHyprcursorManager::registerLoggingFunction(PHYPRCURSORLOGFUNC fn) { logFn = fn; } /* 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; size_t toRead = len > DATA->dataLen - DATA->readNeedle ? DATA->dataLen - DATA->readNeedle : len; std::memcpy(output, (uint8_t*)DATA->data + DATA->readNeedle, toRead); DATA->readNeedle += toRead; return CAIRO_STATUS_SUCCESS; } /* General */ std::optional CHyprcursorImplementation::loadTheme() { if (!themeAccessible(themeFullDir)) return "Theme inaccessible"; // load manifest CManifest manifest(themeFullDir + "/manifest"); const auto PARSERESULT = manifest.parse(); 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()) { Debug::log(HC_LOG_TRACE, logFn, "loadTheme: skipping {}", cursor.path().string()); continue; } auto& SHAPE = theme.shapes.emplace_back(std::make_unique()); auto& LOADEDSHAPE = loadedShapes[SHAPE.get()]; // extract zip to raw data. int errp = 0; zip_t* zip = zip_open(cursor.path().string().c_str(), ZIP_RDONLY, &errp); zip_int64_t index = zip_name_locate(zip, "meta.hl", ZIP_FL_ENC_GUESS); bool metaIsHL = true; if (index == -1) { index = zip_name_locate(zip, "meta.toml", ZIP_FL_ENC_GUESS); metaIsHL = false; } if (index == -1) return "cursor" + cursor.path().string() + "failed to load meta"; zip_file_t* meta_file = zip_fopen_index(zip, index, ZIP_FL_UNCHANGED); 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'; 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; zip_stat_t sb; zip_stat_init(&sb); for (auto& i : SHAPE->images) { if (SHAPE->shapeType == SHAPE_INVALID) { if (i.filename.ends_with(".svg")) SHAPE->shapeType = SHAPE_SVG; else if (i.filename.ends_with(".png")) SHAPE->shapeType = SHAPE_PNG; else { Debug::log(HC_LOG_WARN, logFn, "WARNING: image {} has no known extension, assuming png.", i.filename); SHAPE->shapeType = SHAPE_PNG; } } else { if (SHAPE->shapeType == SHAPE_SVG && !i.filename.ends_with(".svg")) return "meta invalid: cannot add .png files to an svg shape"; else if (SHAPE->shapeType == SHAPE_PNG && i.filename.ends_with(".svg")) return "meta invalid: cannot add .svg files to a png shape"; } // load image Debug::log(HC_LOG_TRACE, logFn, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique()).get(); IMAGE->side = SHAPE->shapeType == SHAPE_SVG ? 0 : i.size; IMAGE->delay = i.delay; IMAGE->isSVG = SHAPE->shapeType == SHAPE_SVG; index = zip_name_locate(zip, i.filename.c_str(), ZIP_FL_ENC_GUESS); if (index == -1) return "cursor" + cursor.path().string() + "failed to load image_file"; // read from zip zip_file_t* image_file = zip_fopen_index(zip, index, ZIP_FL_UNCHANGED); zip_stat_index(zip, index, ZIP_FL_UNCHANGED, &sb); if (sb.valid & ZIP_STAT_SIZE) { IMAGE->data = new char[sb.size + 1]; IMAGE->dataLen = sb.size + 1; } else { IMAGE->data = new char[1024 * 1024]; /* 1MB should be more than enough, again. This probably should be in the spec. */ IMAGE->dataLen = 1024 * 1024; } IMAGE->dataLen = zip_fread(image_file, IMAGE->data, IMAGE->dataLen - 1); zip_fclose(image_file); 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; IMAGE->data = nullptr; return "Failed reading cairoSurface, status " + std::to_string((int)STATUS); } } else { Debug::log(HC_LOG_TRACE, logFn, "Skipping cairo load for a svg surface"); } } if (SHAPE->images.empty()) return "meta invalid: no images for shape " + cursor.path().stem().string(); SHAPE->directory = cursor.path().stem().string(); SHAPE->hotspotX = meta.parsedData.hotspotX; SHAPE->hotspotY = meta.parsedData.hotspotY; SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo); zip_discard(zip); } return {}; } std::vector CHyprcursorImplementation::getFramesFor(SCursorShape* shape, int size) { std::vector frames; for (auto& image : loadedShapes[shape].images) { if (!image->isSVG && image->side != size) continue; if (image->artificial) continue; frames.push_back(image.get()); } return frames; }