mirror of
https://github.com/hyprwm/hyprcursor.git
synced 2024-11-16 18:25:58 +01:00
Initial commit: very basics
This commit is contained in:
parent
47d8cfc1c8
commit
5227dcc787
15 changed files with 1072 additions and 0 deletions
65
.clang-format
Normal file
65
.clang-format
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.vscode/
|
||||
build/
|
71
CMakeLists.txt
Normal file
71
CMakeLists.txt
Normal 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)
|
23
hyprcursor-util/CMakeLists.txt
Normal file
23
hyprcursor-util/CMakeLists.txt
Normal 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)
|
1
hyprcursor-util/internalSharedTypes.hpp
Symbolic link
1
hyprcursor-util/internalSharedTypes.hpp
Symbolic link
|
@ -0,0 +1 @@
|
|||
../libhyprcursor/internalSharedTypes.hpp
|
246
hyprcursor-util/src/main.cpp
Normal file
246
hyprcursor-util/src/main.cpp
Normal 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
10
hyprcursor.pc.in
Normal 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
44
include/hyprcursor.h
Normal 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
62
include/hyprcursor.hpp
Normal 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
53
libhyprcursor/Log.hpp
Normal 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";
|
||||
}
|
||||
};
|
392
libhyprcursor/hyprcursor.cpp
Normal file
392
libhyprcursor/hyprcursor.cpp
Normal 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 {};
|
||||
}
|
17
libhyprcursor/hyprcursor_c.cpp
Normal file
17
libhyprcursor/hyprcursor_c.cpp
Normal 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();
|
||||
}
|
40
libhyprcursor/internalDefines.hpp
Normal file
40
libhyprcursor/internalDefines.hpp
Normal 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();
|
||||
};
|
29
libhyprcursor/internalSharedTypes.hpp
Normal file
29
libhyprcursor/internalSharedTypes.hpp
Normal 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
17
tests/test.cpp
Normal 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();
|
||||
}
|
Loading…
Reference in a new issue