New upstream version 0.1.4
This commit is contained in:
commit
dbdc288df3
|
|
@ -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
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
name: Build Hyprland
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux
|
||||
steps:
|
||||
- name: Checkout repository actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/actions
|
||||
|
||||
- name: Get required pkgs
|
||||
run: |
|
||||
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
|
||||
pacman --noconfirm --noprogressbar -Syyu
|
||||
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang cairo librsvg git libzip
|
||||
|
||||
- name: Install hyprlang
|
||||
run: |
|
||||
git clone https://github.com/hyprwm/hyprlang --recursive
|
||||
cd hyprlang
|
||||
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
|
||||
cmake --build ./build --config Release --target hyprlang -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
|
||||
cmake --install build
|
||||
|
||||
- name: Build hyprcursor
|
||||
run: |
|
||||
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
|
||||
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
|
||||
cmake --install ./build
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
.vscode/
|
||||
build/
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
cmake_minimum_required(VERSION 3.19)
|
||||
|
||||
set(HYPRCURSOR_VERSION "0.1.4")
|
||||
add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}")
|
||||
|
||||
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 librsvg-2.0)
|
||||
|
||||
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/hyprcursor.hpp" "include/hyprcursor/hyprcursor.h" "include/hyprcursor/shared.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/hyprcursor.hpp include/hyprcursor/hyprcursor.h include/hyprcursor/shared.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 in C++" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test)
|
||||
add_dependencies(tests hyprcursor_test)
|
||||
|
||||
add_executable(hyprcursor_test_c "tests/test.c")
|
||||
target_link_libraries(hyprcursor_test_c PRIVATE hyprcursor)
|
||||
add_test(NAME "Test libhyprcursor in C" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test_c)
|
||||
add_dependencies(tests hyprcursor_test_c)
|
||||
|
||||
# Installation
|
||||
install(DIRECTORY "include/hyprcursor" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} DIRECTORY_PERMISSIONS
|
||||
OWNER_WRITE OWNER_READ OWNER_EXECUTE
|
||||
GROUP_WRITE GROUP_READ GROUP_EXECUTE
|
||||
WORLD_WRITE WORLD_READ WORLD_EXECUTE)
|
||||
install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Hypr Development
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
## hyprcursor
|
||||
The hyprland cursor format, library and utilities.
|
||||
|
||||
## Why?
|
||||
|
||||
XCursor sucks, and we still use it today.
|
||||
- Scaling of XCursors is horrible
|
||||
- XCursor does not support vector cursors
|
||||
- XCursor is ridiculously space-inefficient
|
||||
|
||||
Hyprcursor fixes all three. It's an efficient cursor theme format that
|
||||
doesn't suck as much.
|
||||
|
||||
### Notable advantages over XCursor
|
||||
- Automatic scaling according to a configurable, per-cursor method.
|
||||
- Support for SVG cursors
|
||||
- Way more space-efficient. As an example, Bibata-XCursor is 44.1MB, while it's 6.6MB in hyprcursor.
|
||||
|
||||
## Tools
|
||||
|
||||
### hyprcursor-util
|
||||
|
||||
Utility for creating hyprcursor themes. See its readme in `hyprcursor-util/`
|
||||
|
||||
### libhyprcursor
|
||||
|
||||
The library to use for implementing hyprcursors in your compositor or app.
|
||||
|
||||
It provides C and C++ bindings.
|
||||
|
||||
### Examples
|
||||
|
||||
For both C and C++, see `tests/`.
|
||||
|
||||
## Docs
|
||||
|
||||
See `docs/`.
|
||||
|
||||
## TODO
|
||||
|
||||
Library:
|
||||
- [x] Support animated cursors
|
||||
- [x] Support SVG cursors
|
||||
|
||||
Util:
|
||||
- [ ] Support compiling a theme with X
|
||||
- [x] Support decompiling animated cursors
|
||||
|
||||
## Building
|
||||
|
||||
### Deps:
|
||||
- hyprlang >= 0.4.2
|
||||
- cairo
|
||||
- libzip
|
||||
- librsvg
|
||||
|
||||
### Build
|
||||
```sh
|
||||
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
|
||||
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
|
||||
```
|
||||
|
||||
Install with:
|
||||
```sh
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
## Usage
|
||||
|
||||
Include `hyprcursor/hyprcursor.h` or `.hpp` depending on your language.
|
||||
|
||||
Read the comments of the API functions
|
||||
|
||||
Have fun :)
|
||||
|
||||
Doxygen soon I hope :P
|
||||
|
||||
All props exposed are also explained in MAKING_THEMES.md
|
||||
|
||||
## Examples
|
||||
|
||||
See `tests/`.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
## Using a hyprcursor theme
|
||||
|
||||
Download a hyprcursor theme and extract it to a new directory in `~/.local/share/icons`.
|
||||
|
||||
Make sure the first directory contains a manifest, for example:
|
||||
|
||||
```s
|
||||
~/.local/share/icons/myCursorTheme/manifest.hl
|
||||
```
|
||||
|
||||
## Overriding a theme
|
||||
|
||||
Set the `HYPRCURSOR_THEME` env to your theme directory,
|
||||
so for example to get the above to always load, use `export HYPRCURSOR_THEME = myCursorTheme`.
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
## Creating a theme
|
||||
|
||||
Familiarize yourself with the README of `hyprcursor-util`.
|
||||
|
||||
## Creating a theme from an XCursor theme
|
||||
|
||||
Download an XCursor theme, extract it, and then use `--extract`, and then on the resulting output, `--create`.
|
||||
|
||||
Before `--create`, you probably should walk through the `manifest.hl` and all the `meta.hl` files to make sure they're correct,
|
||||
and adjust them to your taste.
|
||||
|
||||
## Creating a theme from scratch
|
||||
|
||||
The directory structure looks like this:
|
||||
```ini
|
||||
directory
|
||||
┣ manifest.hl
|
||||
┗ hyprcursors
|
||||
┣ left_ptr
|
||||
┣ image32.png
|
||||
┣ image64.png
|
||||
┗ meta.hl
|
||||
┣ hand
|
||||
┣ image32.png
|
||||
┣ image64.png
|
||||
┗ meta.hl
|
||||
...
|
||||
```
|
||||
|
||||
### Manifest
|
||||
|
||||
The manifest describes your theme, in hyprlang:
|
||||
```ini
|
||||
name = My theme!
|
||||
description = Very cool!
|
||||
version = 0.1
|
||||
cursors_directory = hyprcursors # has to match the directory in the structure
|
||||
```
|
||||
|
||||
### Cursors
|
||||
|
||||
Each cursor image is a separate directory. In it, multiple size variations can be put.
|
||||
|
||||
`meta.hl` describes the cursor:
|
||||
```ini
|
||||
# what resize algorithm to use when a size is requested
|
||||
# that doesn't match any of your predefined ones.
|
||||
# available: bilinear, nearest, none. None will pick the closest. Nearest is nearest neighbor.
|
||||
resize_algorithm = bilinear
|
||||
|
||||
# "hotspot" is where in your cursor the actual "click point" should be.
|
||||
# this is in absolute coordinates. x+ is east, y+ is north.
|
||||
hotspot_x = 0.0 # this goes 0 - 1
|
||||
hotspot_y = 0.0 # this goes 0 - 1
|
||||
|
||||
# Define what cursor images this one should override.
|
||||
# What this means is that a request for a cursor name e.g. "arrow"
|
||||
# will instead use this one, even if this one is named something else.
|
||||
define_override = arrow
|
||||
define_override = default
|
||||
|
||||
# define your size variants.
|
||||
# Multiple size variants for the same size are treated as an animation.
|
||||
define_size = 64, image64.png
|
||||
define_size = 32, image32.png
|
||||
|
||||
# If you want to animate it, add a timeout in ms at the end:
|
||||
# define_size = 64, anim1.png, 500
|
||||
# define_size = 64, anim2.png, 500
|
||||
# define_size = 64, anim3.png, 500
|
||||
# define_size = 64, anim4.png, 500
|
||||
```
|
||||
|
||||
Supported cursor image types are png and svg.
|
||||
|
||||
If you are using an svg cursor, the size parameter will be ignored.
|
||||
|
||||
Mixing png and svg cursor images in one shape will result in an error.
|
||||
|
||||
Please note animated svgs are not supported, you need to add a separate svg for every frame.
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"nodes": {
|
||||
"hyprlang": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709914708,
|
||||
"narHash": "sha256-bR4o3mynoTa1Wi4ZTjbnsZ6iqVcPGriXp56bZh5UFTk=",
|
||||
"owner": "hyprwm",
|
||||
"repo": "hyprlang",
|
||||
"rev": "a685493fdbeec01ca8ccdf1f3655c044a8ce2fe2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hyprwm",
|
||||
"repo": "hyprlang",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1708475490,
|
||||
"narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0e74ca98a74bc7270d28838369593635a5db3260",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"hyprlang": "hyprlang",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
description = "The hyprland cursor format, library and utilities";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
systems.url = "github:nix-systems/default-linux";
|
||||
|
||||
hyprlang = {
|
||||
url = "github:hyprwm/hyprlang";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
systems,
|
||||
...
|
||||
} @ inputs: let
|
||||
inherit (nixpkgs) lib;
|
||||
eachSystem = lib.genAttrs (import systems);
|
||||
pkgsFor = eachSystem (system:
|
||||
import nixpkgs {
|
||||
localSystem.system = system;
|
||||
overlays = with self.overlays; [default];
|
||||
});
|
||||
in {
|
||||
overlays = import ./nix/overlays.nix {inherit inputs lib;};
|
||||
|
||||
packages = eachSystem (system: {
|
||||
default = self.packages.${system}.hyprcursor;
|
||||
inherit (pkgsFor.${system}) hyprcursor;
|
||||
});
|
||||
|
||||
checks = eachSystem (system: self.packages.${system});
|
||||
|
||||
formatter = eachSystem (system: pkgsFor.${system}.alejandra);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
cmake_minimum_required(VERSION 3.19)
|
||||
|
||||
project(
|
||||
hyprcursor-util
|
||||
DESCRIPTION "A utility for creating and converting hyprcursor themes"
|
||||
)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.0 libzip)
|
||||
add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}")
|
||||
|
||||
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
add_executable(hyprcursor-util ${SRCFILES})
|
||||
|
||||
target_link_libraries(hyprcursor-util PkgConfig::deps)
|
||||
target_include_directories(hyprcursor-util
|
||||
PRIVATE
|
||||
.
|
||||
)
|
||||
|
||||
install(TARGETS hyprcursor-util)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## hyprcursor-util
|
||||
|
||||
A utility to compile, pack, unpack, etc, hyprcursor and xcursor themes.
|
||||
|
||||
## Runtime deps
|
||||
- xcur2png
|
||||
|
||||
## States
|
||||
|
||||
Cursor themes can be in 3 states:
|
||||
- compiled hyprcursor - these can be used by apps / compositors.
|
||||
- compiled xcursor - these can be used by xcursor
|
||||
- working state - an easy to navigate mode where every cursor is a png / svg, and all the meta is in files.
|
||||
|
||||
## Commands
|
||||
|
||||
`--create | -c [path]` -> create a compiled hyprcursor theme from a working state
|
||||
|
||||
`--extract | -x [path]` -> extract an xcursor theme into a working state
|
||||
|
||||
both commands support `--output | -o` to specify an output directory. For safety reasons, **do not use this on versions below 0.1.1** as it will
|
||||
nuke the specified directory without asking.
|
||||
|
||||
Since v0.1.2, this directory is the parent, the theme will be written to a subdirectory in it called `$ACTION_$NAME`.
|
||||
|
||||
### Flags
|
||||
|
||||
`--resize [mode]` - for `extract`: specify a default resize algorithm for shapes. Default is `none`.
|
||||
|
|
@ -0,0 +1 @@
|
|||
../libhyprcursor/internalSharedTypes.hpp
|
||||
|
|
@ -0,0 +1,519 @@
|
|||
#include <iostream>
|
||||
#include <zip.h>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <array>
|
||||
#include <format>
|
||||
#include <algorithm>
|
||||
#include <hyprlang.hpp>
|
||||
#include "internalSharedTypes.hpp"
|
||||
|
||||
enum eOperation {
|
||||
OPERATION_CREATE = 0,
|
||||
OPERATION_EXTRACT = 1,
|
||||
};
|
||||
|
||||
eResizeAlgo explicitResizeAlgo = RESIZE_INVALID;
|
||||
|
||||
struct XCursorConfigEntry {
|
||||
int size = 0, hotspotX = 0, hotspotY = 0, delay = 0;
|
||||
std::string image;
|
||||
};
|
||||
|
||||
static std::string removeBeginEndSpacesTabs(std::string str) {
|
||||
if (str.empty())
|
||||
return str;
|
||||
|
||||
int countBefore = 0;
|
||||
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
|
||||
countBefore++;
|
||||
}
|
||||
|
||||
int countAfter = 0;
|
||||
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
|
||||
countAfter++;
|
||||
}
|
||||
|
||||
str = str.substr(countBefore, str.length() - countBefore - countAfter);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
static bool promptForDeletion(const std::string& path) {
|
||||
|
||||
bool emptyDirectory = !std::filesystem::exists(path);
|
||||
if (!emptyDirectory) {
|
||||
const auto IT = std::filesystem::directory_iterator(path);
|
||||
|
||||
emptyDirectory = !std::count_if(std::filesystem::begin(IT), std::filesystem::end(IT), [](auto& e) { return e.is_regular_file(); });
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(path + "/manifest.hl") && std::filesystem::exists(path) && !emptyDirectory) {
|
||||
std::cout << "Refusing to remove " << path << " because it doesn't look like a hyprcursor theme.\n"
|
||||
<< "Please set a valid, empty, nonexistent, or a theme directory as an output path\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
std::cout << "About to delete (recursively) " << path << ", are you sure? [Y/n]\n";
|
||||
std::string result;
|
||||
std::cin >> result;
|
||||
|
||||
if (result != "Y" && result != "Y\n" && result != "y\n" && result != "y") {
|
||||
std::cout << "Abort.\n";
|
||||
exit(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::remove_all(path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCursorTheme> currentTheme;
|
||||
|
||||
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
|
||||
Hyprlang::CParseResult result;
|
||||
const std::string VALUE = V;
|
||||
|
||||
if (!VALUE.contains(",")) {
|
||||
result.setError("Invalid define_size");
|
||||
return result;
|
||||
}
|
||||
|
||||
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
|
||||
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
|
||||
auto DELAY = 0;
|
||||
|
||||
SCursorImage image;
|
||||
|
||||
if (RHS.contains(",")) {
|
||||
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
|
||||
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
|
||||
|
||||
try {
|
||||
image.delay = std::stoull(RR);
|
||||
} catch (std::exception& e) {
|
||||
result.setError(e.what());
|
||||
return result;
|
||||
}
|
||||
|
||||
RHS = LL;
|
||||
}
|
||||
|
||||
image.filename = RHS;
|
||||
|
||||
try {
|
||||
image.size = std::stoull(LHS);
|
||||
} catch (std::exception& e) {
|
||||
result.setError(e.what());
|
||||
return result;
|
||||
}
|
||||
|
||||
currentTheme->shapes.back()->images.push_back(image);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
|
||||
Hyprlang::CParseResult result;
|
||||
const std::string VALUE = V;
|
||||
|
||||
currentTheme->shapes.back()->overrides.push_back(V);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::optional<std::string> createCursorThemeFromPath(const std::string& path_, const std::string& out_ = {}) {
|
||||
if (!std::filesystem::exists(path_))
|
||||
return "input path does not exist";
|
||||
|
||||
const std::string path = std::filesystem::canonical(path_);
|
||||
|
||||
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->addConfigValue("name", Hyprlang::STRING{""});
|
||||
manifest->addConfigValue("description", Hyprlang::STRING{""});
|
||||
manifest->addConfigValue("version", Hyprlang::STRING{""});
|
||||
manifest->commence();
|
||||
const auto RESULT = manifest->parse();
|
||||
if (RESULT.error)
|
||||
return "Manifest has errors: \n" + std::string{RESULT.getError()};
|
||||
} catch (const char* err) { return "failed parsing manifest: " + std::string{err}; }
|
||||
|
||||
const std::string THEMENAME = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
|
||||
|
||||
std::string out = (out_.empty() ? path.substr(0, path.find_last_of('/') + 1) : out_) + "/theme_" + THEMENAME + "/";
|
||||
|
||||
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
|
||||
const std::string 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::make_unique<SCursorShape>());
|
||||
|
||||
//
|
||||
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();
|
||||
const auto RESULT = meta->parse();
|
||||
|
||||
if (RESULT.error)
|
||||
return "meta.hl has errors: \n" + std::string{RESULT.getError()};
|
||||
} catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; }
|
||||
|
||||
// check if we have at least one image.
|
||||
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 {
|
||||
std::cout << "WARNING: image " << i.filename << " has no known extension, assuming png.\n";
|
||||
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";
|
||||
}
|
||||
|
||||
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 = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
|
||||
|
||||
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
|
||||
promptForDeletion(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 {};
|
||||
}
|
||||
|
||||
static std::string spawnSync(const std::string& cmd) {
|
||||
std::array<char, 128> buffer;
|
||||
std::string result;
|
||||
const std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
|
||||
if (!pipe)
|
||||
return "";
|
||||
|
||||
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
|
||||
result += buffer.data();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::optional<std::string> extractXTheme(const std::string& xpath_, const std::string& out_) {
|
||||
|
||||
if (!spawnSync("xcur2png --help 2>&1").contains("xcursor"))
|
||||
return "missing dependency: -x requires xcur2png.";
|
||||
|
||||
if (!std::filesystem::exists(xpath_) || !std::filesystem::exists(xpath_ + "/cursors"))
|
||||
return "input path does not exist or is not an xcursor theme";
|
||||
|
||||
const std::string xpath = std::filesystem::canonical(xpath_);
|
||||
|
||||
std::string out = (out_.empty() ? xpath.substr(0, xpath.find_last_of('/') + 1) : out_) + "/extracted_" + xpath.substr(xpath.find_last_of('/') + 1) + "/";
|
||||
|
||||
// create output fs structure
|
||||
if (!std::filesystem::exists(out))
|
||||
std::filesystem::create_directory(out);
|
||||
else {
|
||||
// clear the entire thing, avoid melting themes together
|
||||
promptForDeletion(out);
|
||||
std::filesystem::create_directory(out);
|
||||
}
|
||||
|
||||
// write a boring manifest
|
||||
std::ofstream manifest(out + "/manifest.hl", std::ios::trunc);
|
||||
if (!manifest.good())
|
||||
return "failed writing manifest";
|
||||
|
||||
manifest << "name = Extracted Theme\ndescription = Automatically extracted with hyprcursor-util\nversion = 0.1\ncursors_directory = hyprcursors\n";
|
||||
|
||||
manifest.close();
|
||||
|
||||
// make a cursors dir
|
||||
|
||||
std::filesystem::create_directory(out + "/hyprcursors/");
|
||||
|
||||
// create a temp extract dir
|
||||
std::filesystem::create_directory("/tmp/hyprcursor-util/");
|
||||
|
||||
// write all cursors
|
||||
for (auto& xcursor : std::filesystem::directory_iterator(xpath + "/cursors/")) {
|
||||
// ignore symlinks, we'll write them to the meta.hl file.
|
||||
if (!xcursor.is_regular_file() || xcursor.is_symlink())
|
||||
continue;
|
||||
|
||||
const auto CURSORDIR = out + "/hyprcursors/" + xcursor.path().stem().string();
|
||||
std::filesystem::create_directory(CURSORDIR);
|
||||
|
||||
std::cout << "Found xcursor " << xcursor.path().stem().string() << "\n";
|
||||
|
||||
// decompile xcursor
|
||||
const auto OUT = spawnSync(std::format("rm -f /tmp/hyprcursor-util/* && cd /tmp/hyprcursor-util && xcur2png {} -d /tmp/hyprcursor-util 2>&1",
|
||||
std::filesystem::canonical(xcursor.path()).string()));
|
||||
|
||||
// read the config
|
||||
std::vector<XCursorConfigEntry> entries;
|
||||
std::ifstream xconfig("/tmp/hyprcursor-util/" + xcursor.path().stem().string() + ".conf");
|
||||
if (!xconfig.good())
|
||||
return "Failed reading xconfig for " + xcursor.path().string();
|
||||
|
||||
std::string line = "";
|
||||
|
||||
while (std::getline(xconfig, line)) {
|
||||
if (line.starts_with("#"))
|
||||
continue;
|
||||
|
||||
auto& ENTRY = entries.emplace_back();
|
||||
|
||||
// extract
|
||||
try {
|
||||
std::string curval = line.substr(0, line.find_first_of('\t'));
|
||||
ENTRY.size = std::stoi(curval);
|
||||
line = line.substr(line.find_first_of('\t') + 1);
|
||||
|
||||
curval = line.substr(0, line.find_first_of('\t'));
|
||||
ENTRY.hotspotX = std::stoi(curval);
|
||||
line = line.substr(line.find_first_of('\t') + 1);
|
||||
|
||||
curval = line.substr(0, line.find_first_of('\t'));
|
||||
ENTRY.hotspotY = std::stoi(curval);
|
||||
line = line.substr(line.find_first_of('\t') + 1);
|
||||
|
||||
curval = line.substr(0, line.find_first_of('\t'));
|
||||
ENTRY.image = curval;
|
||||
line = line.substr(line.find_first_of('\t') + 1);
|
||||
|
||||
curval = line.substr(0, line.find_first_of('\t'));
|
||||
ENTRY.delay = std::stoi(curval);
|
||||
} catch (std::exception& e) { return "Failed reading xconfig " + xcursor.path().string() + " because of " + e.what(); }
|
||||
|
||||
std::cout << "Extracted " << xcursor.path().stem().string() << " at size " << ENTRY.size << "\n";
|
||||
}
|
||||
|
||||
if (entries.empty())
|
||||
return "Empty xcursor " + xcursor.path().string();
|
||||
|
||||
// copy pngs
|
||||
for (auto& extracted : std::filesystem::directory_iterator("/tmp/hyprcursor-util")) {
|
||||
if (extracted.path().string().ends_with(".conf"))
|
||||
continue;
|
||||
|
||||
std::filesystem::copy(extracted, CURSORDIR + "/");
|
||||
}
|
||||
|
||||
// write a meta.hl
|
||||
std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo));
|
||||
|
||||
// find hotspot from first entry
|
||||
metaString +=
|
||||
std::format("hotspot_x = {:.2f}\nhotspot_y = {:.2f}\n\n", (float)entries[0].hotspotX / (float)entries[0].size, (float)entries[0].hotspotY / (float)entries[0].size);
|
||||
|
||||
// define all sizes
|
||||
for (auto& entry : entries) {
|
||||
const auto ENTRYSTEM = entry.image.substr(entry.image.find_last_of('/') + 1);
|
||||
|
||||
metaString += std::format("define_size = {}, {}, {}\n", entry.size, ENTRYSTEM, entry.delay);
|
||||
}
|
||||
|
||||
metaString += "\n";
|
||||
|
||||
// define overrides, scan for symlinks
|
||||
|
||||
for (auto& xcursor2 : std::filesystem::directory_iterator(xpath + "/cursors/")) {
|
||||
if (!xcursor2.is_symlink())
|
||||
continue;
|
||||
|
||||
if (std::filesystem::canonical(xcursor2) != std::filesystem::canonical(xcursor))
|
||||
continue;
|
||||
|
||||
// this sym points to us
|
||||
metaString += std::format("define_override = {}\n", xcursor2.path().stem().string());
|
||||
}
|
||||
|
||||
// meta done, write
|
||||
std::ofstream meta(CURSORDIR + "/meta.hl", std::ios::trunc);
|
||||
meta << metaString;
|
||||
meta.close();
|
||||
}
|
||||
|
||||
std::filesystem::remove_all("/tmp/hyprcursor-util/");
|
||||
|
||||
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 (arg == "-v" || arg == "--version") {
|
||||
std::cout << "hyprcursor-util, built from v" << HYPRCURSOR_VERSION << "\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
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 if (arg == "--extract" || arg == "-x") {
|
||||
op = OPERATION_EXTRACT;
|
||||
|
||||
if (argc < 3) {
|
||||
std::cerr << "Missing path for extract.\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 if (arg == "--resize") {
|
||||
explicitResizeAlgo = stringToAlgo(argv[++i]);
|
||||
continue;
|
||||
} else {
|
||||
std::cerr << "Unknown arg: " << arg << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (path.ends_with("/"))
|
||||
path.pop_back();
|
||||
|
||||
switch (op) {
|
||||
case OPERATION_CREATE: {
|
||||
const auto RET = createCursorThemeFromPath(path, out);
|
||||
if (RET.has_value()) {
|
||||
std::cerr << "Failed: " << RET.value() << "\n";
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OPERATION_EXTRACT: {
|
||||
const auto RET = extractXTheme(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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
#ifndef HYPRCURSOR_H
|
||||
#define HYPRCURSOR_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
#define CAPI extern "C"
|
||||
|
||||
#else
|
||||
|
||||
#define CAPI
|
||||
|
||||
#endif
|
||||
|
||||
#include "shared.h"
|
||||
|
||||
struct hyprcursor_manager_t;
|
||||
|
||||
/*!
|
||||
Simple struct for styles
|
||||
*/
|
||||
struct hyprcursor_cursor_style_info {
|
||||
/*!
|
||||
Shape size.
|
||||
0 means "any" or "unspecified".
|
||||
*/
|
||||
unsigned int size;
|
||||
};
|
||||
|
||||
/*!
|
||||
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 struct hyprcursor_manager_t* hyprcursor_manager_create(const char* theme_name);
|
||||
|
||||
/*!
|
||||
Free a hyprcursor_manager_t*
|
||||
*/
|
||||
CAPI void hyprcursor_manager_free(struct hyprcursor_manager_t* manager);
|
||||
|
||||
/*!
|
||||
Returns true if the theme was successfully loaded,
|
||||
i.e. everything is A-OK and nothing should fail.
|
||||
*/
|
||||
CAPI int hyprcursor_manager_valid(struct hyprcursor_manager_t* manager);
|
||||
|
||||
/*!
|
||||
Loads a theme at a given style, synchronously.
|
||||
|
||||
Returns whether it succeeded.
|
||||
*/
|
||||
CAPI int hyprcursor_load_theme_style(struct hyprcursor_manager_t* manager, struct hyprcursor_cursor_style_info info);
|
||||
|
||||
/*!
|
||||
Returns a hyprcursor_cursor_image_data*[] for a given cursor
|
||||
shape and size.
|
||||
|
||||
The entire array needs to be freed instantly after using, see hyprcursor_cursor_image_data_free()
|
||||
|
||||
Surfaces stay valid.
|
||||
|
||||
Once done with a size, call hyprcursor_style_done()
|
||||
*/
|
||||
CAPI hyprcursor_cursor_image_data** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info, int* out_size);
|
||||
|
||||
/*!
|
||||
Free a returned hyprcursor_cursor_image_data.
|
||||
*/
|
||||
CAPI void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data, int size);
|
||||
|
||||
/*!
|
||||
Marks a certain style as done, allowing it to be potentially freed
|
||||
*/
|
||||
CAPI void hyprcursor_style_done(struct hyprcursor_manager_t* manager, struct hyprcursor_cursor_style_info info);
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "shared.h"
|
||||
|
||||
class CHyprcursorImplementation;
|
||||
|
||||
namespace Hyprcursor {
|
||||
|
||||
/*!
|
||||
Simple struct for styles
|
||||
*/
|
||||
struct SCursorStyleInfo {
|
||||
/*!
|
||||
Shape size.
|
||||
|
||||
0 means "any" or "unspecified".
|
||||
*/
|
||||
unsigned int size = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
struct for cursor shape data
|
||||
*/
|
||||
struct SCursorShapeData {
|
||||
std::vector<SCursorImageData> images;
|
||||
};
|
||||
|
||||
/*!
|
||||
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();
|
||||
|
||||
/*!
|
||||
Loads this theme at a given style, synchronously.
|
||||
|
||||
Returns whether it succeeded.
|
||||
*/
|
||||
bool loadThemeStyle(const SCursorStyleInfo& info);
|
||||
|
||||
/*!
|
||||
Returns the shape data struct for a given
|
||||
style.
|
||||
|
||||
Once done with a style, call cursorSurfaceDone()
|
||||
|
||||
The surfaces references stay valid until cursorSurfaceStyleDone() is called on the owning style.
|
||||
*/
|
||||
SCursorShapeData getShape(const char* shape, const SCursorStyleInfo& info) {
|
||||
int size = 0;
|
||||
SCursorImageData** images = getShapesC(size, shape, info);
|
||||
|
||||
SCursorShapeData data;
|
||||
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
SCursorImageData image;
|
||||
image.delay = images[i]->delay;
|
||||
image.size = images[i]->size;
|
||||
image.surface = images[i]->surface;
|
||||
image.hotspotX = images[i]->hotspotX;
|
||||
image.hotspotY = images[i]->hotspotY;
|
||||
data.images.push_back(image);
|
||||
|
||||
free(images[i]);
|
||||
}
|
||||
|
||||
free(images);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/*!
|
||||
Prefer getShape, this is for C compat.
|
||||
*/
|
||||
SCursorImageData** getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info);
|
||||
|
||||
/*!
|
||||
Marks a certain style as done, allowing it to be potentially freed
|
||||
*/
|
||||
void cursorSurfaceStyleDone(const SCursorStyleInfo&);
|
||||
|
||||
private:
|
||||
CHyprcursorImplementation* impl = nullptr;
|
||||
bool finalizedAndValid = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#include <cairo/cairo.h>
|
||||
|
||||
#ifndef HYPRCURSOR_SHARED_H
|
||||
#define HYPRCURSOR_SHARED_H
|
||||
|
||||
/*!
|
||||
struct for a single cursor image
|
||||
*/
|
||||
struct SCursorImageData {
|
||||
cairo_surface_t* surface;
|
||||
int size;
|
||||
int delay;
|
||||
int hotspotX;
|
||||
int hotspotY;
|
||||
};
|
||||
|
||||
typedef struct SCursorImageData hyprcursor_cursor_image_data;
|
||||
|
||||
#endif
|
||||
|
|
@ -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";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,677 @@
|
|||
#include "hyprcursor/hyprcursor.hpp"
|
||||
#include "internalSharedTypes.hpp"
|
||||
#include "internalDefines.hpp"
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <hyprlang.hpp>
|
||||
#include <zip.h>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <librsvg/rsvg.h>
|
||||
|
||||
#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 bool themeAccessible(const std::string& path) {
|
||||
try {
|
||||
if (!std::filesystem::exists(path + "/manifest.hl"))
|
||||
return false;
|
||||
|
||||
} catch (std::exception& e) { return false; }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (!name.empty() && themeDir.path().stem().string() != name)
|
||||
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;
|
||||
|
||||
if (!name.empty() && themeDir.path().stem().string() != name)
|
||||
continue;
|
||||
|
||||
if (!themeAccessible(themeDir.path().string()))
|
||||
continue;
|
||||
|
||||
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
|
||||
|
||||
if (std::filesystem::exists(MANIFESTPATH))
|
||||
return std::filesystem::canonical(themeDir.path()).string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!name.empty()) // try without name
|
||||
return getFullPathForThemeName("");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info) {
|
||||
std::string REQUESTEDSHAPE = shape_;
|
||||
|
||||
std::vector<SLoadedCursorImage*> 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;
|
||||
|
||||
hotX = shape->hotspotX;
|
||||
hotY = shape->hotspotY;
|
||||
|
||||
// matched :)
|
||||
bool foundAny = false;
|
||||
for (auto& image : impl->loadedShapes[shape.get()].images) {
|
||||
if (image->side != info.size)
|
||||
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 != RESIZE_NONE) {
|
||||
Debug::log(ERR, "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 - info.size)) > leader)
|
||||
continue;
|
||||
|
||||
leader = image->side;
|
||||
}
|
||||
|
||||
if (leader == 13371337) { // ???
|
||||
Debug::log(ERR, "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(ERR, "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 = hotX * data[i]->size;
|
||||
data[i]->hotspotY = hotY * data[i]->size;
|
||||
}
|
||||
|
||||
outSize = resultingImages.size();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
|
||||
for (auto& shape : impl->theme.shapes) {
|
||||
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG)
|
||||
continue; // don't resample NONE style cursors
|
||||
|
||||
bool sizeFound = false;
|
||||
|
||||
if (shape->shapeType == SHAPE_PNG) {
|
||||
for (auto& image : impl->loadedShapes[shape.get()].images) {
|
||||
if (image->side != info.size)
|
||||
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(ERR, "Resampling failed to find a candidate???");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto FRAMES = impl->getFramesFor(shape.get(), leader->side);
|
||||
|
||||
for (auto& f : FRAMES) {
|
||||
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
|
||||
newImage->artificial = true;
|
||||
newImage->side = info.size;
|
||||
newImage->artificialData = new char[info.size * info.size * 4];
|
||||
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
|
||||
newImage->delay = f->delay;
|
||||
|
||||
const auto PCAIRO = cairo_create(newImage->cairoSurface);
|
||||
|
||||
cairo_set_antialias(PCAIRO, shape->resizeAlgo == 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 = info.size / (float)f->side;
|
||||
cairo_scale(PCAIRO, scale, scale);
|
||||
cairo_pattern_set_filter(PTN, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
|
||||
cairo_set_source(PCAIRO, PTN);
|
||||
|
||||
cairo_rectangle(PCAIRO, 0, 0, info.size, info.size);
|
||||
|
||||
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);
|
||||
|
||||
for (auto& f : FRAMES) {
|
||||
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
|
||||
newImage->artificial = true;
|
||||
newImage->side = info.size;
|
||||
newImage->artificialData = new char[info.size * info.size * 4];
|
||||
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 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(ERR, "Failed reading svg: {}", error->message);
|
||||
return false;
|
||||
}
|
||||
|
||||
RsvgRectangle rect = {0, 0, (double)info.size, (double)info.size};
|
||||
|
||||
if (!rsvg_handle_render_document(handle, PCAIRO, &rect, &error)) {
|
||||
Debug::log(ERR, "Failed rendering svg: {}", error->message);
|
||||
return false;
|
||||
}
|
||||
|
||||
// done
|
||||
cairo_surface_flush(newImage->cairoSurface);
|
||||
cairo_destroy(PCAIRO);
|
||||
}
|
||||
} else {
|
||||
Debug::log(ERR, "Invalid shapetype in loadThemeStyle");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
|
||||
for (auto& shape : impl->theme.shapes) {
|
||||
if (shape->resizeAlgo == 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 == info.size)
|
||||
return true;
|
||||
|
||||
// clean invalid non-svg rasters
|
||||
if (!isSVG && e->side == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Implementation
|
||||
|
||||
*/
|
||||
|
||||
static std::string removeBeginEndSpacesTabs(std::string str) {
|
||||
if (str.empty())
|
||||
return str;
|
||||
|
||||
int countBefore = 0;
|
||||
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
|
||||
countBefore++;
|
||||
}
|
||||
|
||||
int countAfter = 0;
|
||||
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
|
||||
countAfter++;
|
||||
}
|
||||
|
||||
str = str.substr(countBefore, str.length() - countBefore - countAfter);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
SCursorTheme* currentTheme;
|
||||
|
||||
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
|
||||
Hyprlang::CParseResult result;
|
||||
const std::string VALUE = V;
|
||||
|
||||
if (!VALUE.contains(",")) {
|
||||
result.setError("Invalid define_size");
|
||||
return result;
|
||||
}
|
||||
|
||||
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
|
||||
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
|
||||
auto DELAY = 0;
|
||||
|
||||
SCursorImage image;
|
||||
|
||||
if (RHS.contains(",")) {
|
||||
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
|
||||
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
|
||||
|
||||
try {
|
||||
image.delay = std::stoull(RR);
|
||||
} catch (std::exception& e) {
|
||||
result.setError(e.what());
|
||||
return result;
|
||||
}
|
||||
|
||||
RHS = LL;
|
||||
}
|
||||
|
||||
image.filename = RHS;
|
||||
|
||||
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(TRACE, "cairo: png read, freeing mem");
|
||||
}
|
||||
|
||||
return CAIRO_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
General
|
||||
|
||||
*/
|
||||
|
||||
std::optional<std::string> CHyprcursorImplementation::loadTheme() {
|
||||
|
||||
if (!themeAccessible(themeFullDir))
|
||||
return "Theme inaccessible";
|
||||
|
||||
currentTheme = &theme;
|
||||
|
||||
// load manifest
|
||||
std::unique_ptr<Hyprlang::CConfig> manifest;
|
||||
try {
|
||||
// TODO: unify this between util and lib
|
||||
manifest = std::make_unique<Hyprlang::CConfig>((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{});
|
||||
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
|
||||
manifest->commence();
|
||||
manifest->parse();
|
||||
} catch (const char* err) {
|
||||
Debug::log(ERR, "Failed parsing manifest due to {}", err);
|
||||
return std::string{"failed: "} + err;
|
||||
}
|
||||
|
||||
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(std::make_unique<SCursorShape>());
|
||||
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_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) {
|
||||
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 {
|
||||
std::cout << "WARNING: image " << i.filename << " has no known extension, assuming png.\n";
|
||||
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(TRACE, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
|
||||
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get();
|
||||
IMAGE->side = i.size;
|
||||
IMAGE->delay = i.delay;
|
||||
IMAGE->isSVG = SHAPE->shapeType == SHAPE_SVG;
|
||||
|
||||
// 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(TRACE, "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(LOG, "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 = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
|
||||
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
|
||||
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
|
||||
|
||||
zip_discard(zip);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<SLoadedCursorImage*> CHyprcursorImplementation::getFramesFor(SCursorShape* shape, int size) {
|
||||
std::vector<SLoadedCursorImage*> 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#include "hyprcursor/hyprcursor.h"
|
||||
#include "hyprcursor/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;
|
||||
}
|
||||
|
||||
int hyprcursor_manager_valid(hyprcursor_manager_t* manager) {
|
||||
const auto MGR = (CHyprcursorManager*)manager;
|
||||
return MGR->valid();
|
||||
}
|
||||
|
||||
int hyprcursor_load_theme_style(hyprcursor_manager_t* manager, hyprcursor_cursor_style_info info_) {
|
||||
const auto MGR = (CHyprcursorManager*)manager;
|
||||
SCursorStyleInfo info;
|
||||
info.size = info_.size;
|
||||
return MGR->loadThemeStyle(info);
|
||||
}
|
||||
|
||||
struct SCursorImageData** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info_, int* out_size) {
|
||||
const auto MGR = (CHyprcursorManager*)manager;
|
||||
SCursorStyleInfo info;
|
||||
info.size = info_.size;
|
||||
int size = 0;
|
||||
struct SCursorImageData** data = MGR->getShapesC(size, shape, info);
|
||||
*out_size = size;
|
||||
return data;
|
||||
}
|
||||
|
||||
void hyprcursor_cursor_image_data_free(hyprcursor_cursor_image_data** data, int size) {
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
free(data[i]);
|
||||
}
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
void hyprcursor_style_done(hyprcursor_manager_t* manager, hyprcursor_cursor_style_info info_) {
|
||||
const auto MGR = (CHyprcursorManager*)manager;
|
||||
SCursorStyleInfo info;
|
||||
info.size = info_.size;
|
||||
return MGR->cursorSurfaceStyleDone(info);
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include "internalSharedTypes.hpp"
|
||||
#include <optional>
|
||||
#include <cairo/cairo.h>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
|
||||
struct SLoadedCursorImage {
|
||||
~SLoadedCursorImage() {
|
||||
if (data)
|
||||
delete[] (char*)data;
|
||||
if (artificialData)
|
||||
delete[] (char*)artificialData;
|
||||
if (cairoSurface)
|
||||
cairo_surface_destroy(cairoSurface);
|
||||
}
|
||||
|
||||
// read stuff
|
||||
size_t readNeedle = 0;
|
||||
void* data = nullptr;
|
||||
size_t dataLen = 0;
|
||||
bool isSVG = false; // if true, data is just a string of chars
|
||||
|
||||
cairo_surface_t* cairoSurface = nullptr;
|
||||
int side = 0;
|
||||
int delay = 0;
|
||||
|
||||
// means this was created by resampling
|
||||
void* artificialData = nullptr;
|
||||
bool artificial = false;
|
||||
};
|
||||
|
||||
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();
|
||||
std::vector<SLoadedCursorImage*> getFramesFor(SCursorShape* shape, int size);
|
||||
};
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
enum eResizeAlgo {
|
||||
RESIZE_INVALID = 0,
|
||||
RESIZE_NONE,
|
||||
RESIZE_BILINEAR,
|
||||
RESIZE_NEAREST,
|
||||
};
|
||||
|
||||
enum eShapeType {
|
||||
SHAPE_INVALID = 0,
|
||||
SHAPE_PNG,
|
||||
SHAPE_SVG,
|
||||
};
|
||||
|
||||
inline eResizeAlgo stringToAlgo(const std::string& s) {
|
||||
if (s == "none")
|
||||
return RESIZE_NONE;
|
||||
if (s == "nearest")
|
||||
return RESIZE_NEAREST;
|
||||
return RESIZE_BILINEAR;
|
||||
}
|
||||
|
||||
inline std::string algoToString(const eResizeAlgo a) {
|
||||
switch (a) {
|
||||
case RESIZE_BILINEAR: return "bilinear";
|
||||
case RESIZE_NEAREST: return "nearest";
|
||||
case RESIZE_NONE: return "none";
|
||||
default: return "none";
|
||||
}
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
struct SCursorImage {
|
||||
std::string filename;
|
||||
int size = 0;
|
||||
int delay = 0;
|
||||
};
|
||||
|
||||
struct SCursorShape {
|
||||
std::string directory;
|
||||
float hotspotX = 0, hotspotY = 0;
|
||||
eResizeAlgo resizeAlgo = RESIZE_NEAREST;
|
||||
std::vector<SCursorImage> images;
|
||||
std::vector<std::string> overrides;
|
||||
eShapeType shapeType = SHAPE_INVALID;
|
||||
};
|
||||
|
||||
struct SCursorTheme {
|
||||
std::vector<std::unique_ptr<SCursorShape>> shapes;
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
lib,
|
||||
stdenv,
|
||||
cmake,
|
||||
pkg-config,
|
||||
cairo,
|
||||
hyprlang,
|
||||
librsvg,
|
||||
libzip,
|
||||
version ? "git",
|
||||
}:
|
||||
stdenv.mkDerivation {
|
||||
pname = "hyprcursor";
|
||||
inherit version;
|
||||
src = ../.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
cmake
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
cairo
|
||||
hyprlang
|
||||
librsvg
|
||||
libzip
|
||||
];
|
||||
|
||||
outputs = [
|
||||
"out"
|
||||
"dev"
|
||||
"lib"
|
||||
];
|
||||
|
||||
meta = {
|
||||
homepage = "https://github.com/hyprwm/hyprcursor";
|
||||
description = "The hyprland cursor format, library and utilities";
|
||||
license = lib.licenses.bsd3;
|
||||
platforms = lib.platforms.linux;
|
||||
mainProgram = "hyprcursor";
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
lib,
|
||||
inputs,
|
||||
}: let
|
||||
mkDate = longDate: (lib.concatStringsSep "-" [
|
||||
(builtins.substring 0 4 longDate)
|
||||
(builtins.substring 4 2 longDate)
|
||||
(builtins.substring 6 2 longDate)
|
||||
]);
|
||||
in {
|
||||
default = inputs.self.overlays.hyprcursor;
|
||||
|
||||
hyprcursor = lib.composeManyExtensions [
|
||||
inputs.hyprlang.overlays.default
|
||||
(final: prev: {
|
||||
hyprcursor = prev.callPackage ./default.nix {
|
||||
stdenv = prev.gcc13Stdenv;
|
||||
version = "0.pre" + "+date=" + (mkDate (inputs.self.lastModifiedDate or "19700101")) + "_" + (inputs.self.shortRev or "dirty");
|
||||
inherit (final) hyprlang;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
#include <hyprcursor/hyprcursor.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
struct hyprcursor_manager_t* mgr = hyprcursor_manager_create(NULL);
|
||||
|
||||
if (!mgr) {
|
||||
printf("mgr null\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct hyprcursor_cursor_style_info info = {.size = 48};
|
||||
if (!hyprcursor_load_theme_style(mgr, info)) {
|
||||
printf("load failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dataSize = 0;
|
||||
hyprcursor_cursor_image_data** data = hyprcursor_get_cursor_image_data(mgr, "left_ptr", info, &dataSize);
|
||||
if (data == NULL) {
|
||||
printf("data failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ret = cairo_surface_write_to_png(data[0]->surface, "/tmp/arrowC.png");
|
||||
|
||||
hyprcursor_cursor_image_data_free(data, dataSize);
|
||||
hyprcursor_style_done(mgr, info);
|
||||
|
||||
if (ret) {
|
||||
printf("cairo failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#include <iostream>
|
||||
#include <hyprcursor/hyprcursor.hpp>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Hyprcursor::CHyprcursorManager mgr(nullptr);
|
||||
|
||||
Hyprcursor::SCursorStyleInfo style{.size = 48};
|
||||
|
||||
// preload size 48 for testing
|
||||
if (!mgr.loadThemeStyle(style)) {
|
||||
std::cout << "failed loading style\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// get cursor for left_ptr
|
||||
const auto SHAPEDATA = mgr.getShape("left_ptr", style);
|
||||
|
||||
if (SHAPEDATA.images.empty()) {
|
||||
std::cout << "no images\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n";
|
||||
|
||||
// save to disk
|
||||
const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png");
|
||||
|
||||
std::cout << "Cairo returned for write: " << RET << "\n";
|
||||
|
||||
mgr.cursorSurfaceStyleDone(style);
|
||||
|
||||
if (RET)
|
||||
return 1;
|
||||
|
||||
return !mgr.valid();
|
||||
}
|
||||
Loading…
Reference in New Issue