Compare commits

..

62 commits
v0.8.4 ... main

Author SHA1 Message Date
a2dbd8a4cc
version: bump to 0.13.1 2026-05-08 11:06:10 +01:00
3e170e5ad0
path: add function overloads with extension param (#109) 2026-05-07 19:49:39 +01:00
ec5c0c7097
version: bump to 0.13.0 2026-04-29 20:51:26 +01:00
Lee Bousfield
ee9ef3d43e
math: CBox::overlaps should not count adjacent boxes (#108) 2026-04-26 19:14:32 +01:00
Vaxry
fe6183f314
animations: add spring controlled curves (#105)
Adds spring-controlled curves to the animation manager.
2026-04-26 18:31:30 +01:00
Visal Vijay
afb9009312
process: handle execvp failure in runAsync (#107) 2026-04-26 10:57:51 +01:00
Visal Vijay
fa3992be2d
process: free argv allocations on execvp failure (#106)
* process: free argv allocations on execvp failure

* fix execvp style
2026-04-25 21:17:03 +01:00
eedd60805c
CI: use org-wide actions 2026-04-17 15:27:46 +03:00
e6caa3d4d1
version: bump to 0.12.0 2026-03-30 23:56:31 +01:00
762166b516
string/varlist2: add view ctors 2026-03-26 14:20:54 +00:00
Hiroki Tagato
cb4e152dc7
internal: add libc++ < 20 fallback for floating-point strToNumber (#104)
* Add missing includes.

* Add libc++ < 20 fallback for floating-point strToNumber

libc++ prior to version 20 does not implement std::from_chars for
floating point types, which causes builds to fail on systems using older
libc++ versions (e.g. FreeBSD).

Add a small fallback using strtof/strtod/strtold for building with
libc++ < 20.

While libstdc++, or libc++ 20 or later continue to use the existing
std::from_chars implementation, this change restores compatibility with
older libc++ versions.
2026-03-22 20:30:05 +00:00
b85b779e3e
version: bump to 0.11.1 2026-03-19 19:26:04 +00:00
668b22df50
string: add isNumber2 for sv 2026-03-19 16:19:10 +00:00
6b4c47661e
path: add resolvePath 2026-03-17 14:58:44 -04:00
d32196ab2a
string/numeric: add hex parsing 2026-03-17 14:52:42 -04:00
7d3be08f84
string/numeric: add numeric parsing 2026-03-17 11:35:43 -04:00
Linux User
5e228db682
i18n: include vector header (#103)
Fixes error `no member named 'vector' in namespace 'std'` on llvm/musl
2026-03-13 16:11:03 -05:00
8eb974bdea
memory: add dynamicPointerCast for weak ptrs
closes #102
2026-03-07 18:28:56 +00:00
e63f3a7933
treewide: alejandra -> nixfmt 2026-03-02 15:57:50 +02:00
ancorehraq
340a792e3b
logger: bump to C++26, set FD_CLOEXEC on log file descriptor (#100) 2026-02-16 19:51:27 +00:00
9038eec033
memory: fix a few UAF cases, add asan to test 2026-02-03 17:30:57 +00:00
Tom Englund
51a4f93ce8
signals: check if listeners is empty in emitInternal (#96)
* signals: use tuple reference and check if listeners is empty

use forward_as_tuple as ít creates a temporary tuple reference instead
of copying/moving each argument. also if guard the emitInternal to only
create locals if there is actually any listeners.

* signal: revert forward_as_tuple

cant use references as signals expect a copy/move.
2025-12-20 17:56:12 +00:00
Tom Englund
5ac060bfcf
signal: check for trivially copyable (#95)
use is_trivially_copyable instead because is_arithmetic_v exludes small
POD structs, enums, and certain trivially copyable types.

also add a if guard in emit if we have no listeners, no point emitting.
2025-12-19 16:12:51 +00:00
1c527b30fe
cli/logger: flush stdout after logging 2025-12-17 20:09:42 +00:00
fe686486ac
version: bump to 0.11.0 2025-12-05 19:18:01 +00:00
2f2413801b
logger: don't crash on failing to print to stdout 2025-12-02 00:58:52 +00:00
Vaxry
9f8e158dbd
memory/shared: add dynamicPointerCast (#92) 2025-12-01 21:18:31 +00:00
EvilLary
7e6346f84b
i18n: fix typo in sorting entries (#94) 2025-11-30 00:54:28 +00:00
Maximilian Seidler
a64517c236
animation: allow/intend for animated vars to be unique pointers (#93) 2025-11-28 17:08:50 +00:00
0168583075
cli/argumentParser: improve formatting of description 2025-11-24 14:54:18 +00:00
5e1a14bc29
cli/argumentParser: allow empty short 2025-11-24 14:44:43 +00:00
bc9803c4b8
version: bump to 0.10.4 2025-11-24 14:13:22 +00:00
SASANO Takayoshi
44c2ba0354
src/cli/ArgumentParser.cpp: clang requires <algorithm> (#91) 2025-11-24 13:53:20 +00:00
96df6f6535
cli/logger: add redirection of connections 2025-11-23 18:30:59 +00:00
a9fe9748ae
version: bump to 0.10.3 2025-11-23 16:03:58 +00:00
Vaxry
b311dc90dc
cli: add logger (#90)
Adds a Logger to the CLI namespace
2025-11-23 15:45:14 +00:00
Vaxry
16a7fe760d
cli: add CArgumentParser (#89) 2025-11-23 00:20:24 +00:00
31f29957df
Nix: always test in debug mode 2025-11-22 00:35:18 +02:00
1454845751
cmake: add coverage reports 2025-11-21 22:19:01 +00:00
Vaxry
2698ac1194
tests: move tests out of source tree (#88) 2025-11-21 16:27:29 +00:00
671792bcfe
i18n: fix error with localizing multi-var strings 2025-11-19 00:51:03 +00:00
0c6411851c
i18n/engine: optimize string generation 2025-11-16 20:02:11 +00:00
e3cae692f6
i18n/engine: fix pure stem matches 2025-11-15 22:21:40 +00:00
Constantin Piber
cb3e797fde
internal: fix missing headers (#86) 2025-11-10 22:02:48 +00:00
0a28f35c00
version: bump to 0.10.2 2025-11-10 13:00:04 +00:00
Vaxry
01afe9245b
tests: move to gtest (#85)
---------

Co-authored-by: Mihai Fufezan <mihai@fufexan.net>
2025-11-09 18:05:01 +00:00
Vaxry
9a9745d7aa
string: add VarList2 (#84)
reworks how varList works, mixes best of both ConstVarList and regular VarList. Deprecates both.
2025-11-09 15:10:58 +00:00
Vaxry
968f881222
i18n: add i18n engine (#83)
Adds a translation engine
2025-11-09 14:18:33 +00:00
926689ddb9
version: bump to 0.10.1 2025-11-06 00:09:00 +00:00
9637961a55
string/constVarList: fix UAF 2025-11-05 15:04:02 +00:00
Freevatar
164a30b3d8
animation/bezier: Fix OOB in getYForPoint for non-monotonic 4-point curves (#81) 2025-11-03 22:25:56 +00:00
3df7bde01e
version: bump to 0.10.0 2025-10-05 00:12:03 +01:00
9ab64319e9
math/region: reinit region for scale()
Apparently on some setups directly modifying the rectangles is a no-go, see #78.

Pixman is another piece of undocumented shit. I hope whomever wrote this stubs their toe.

Note to self: drop pixman, rewrite region. Fucking idiots.
2025-10-04 21:22:03 +01:00
Maximilian Seidler
feaaf44d59
memory: make SP/ASP control blocks type agnostic (#79) 2025-10-04 20:35:48 +02:00
94cce79434
tests/math: add region scale test 2025-10-03 12:28:46 +01:00
c1f541256e
version: bump to 0.9.0 2025-10-03 12:16:41 +01:00
1f80045da1
bezier: fix with first point being non-0 2025-09-30 15:05:40 +01:00
a20932e200
bezier: add setup4 2025-09-30 13:56:37 +01:00
Tom Englund
64446e1a4c vector2d: make vector trivial
while profiling vector showed up as a marginal waster because it wasnt a
trivial class, a few if(m_someVector != Vector()) was causing a lot of
churn because of not being trivial and doing a lot of allocaitons and
destructions. make it trivial by defaulting constructor, and destructor,
and while we are at it make it constexpr friendly on constructors and
operators.
2025-09-28 00:01:31 +02:00
Tom Englund
05f0fb2774 memory: fix clangd warnings
modernize validHierarchy and isConstructible, mark move operator
noexcept, use default destructor instead of {}, __deleter is a reserved
name, remove a underscore.

as per clangd docs.
The C standard additionally reserves names beginning with a double underscore,
while the C++ standard strengthens this to reserve names with a double underscore occurring anywhere.
2025-09-27 01:01:56 +02:00
Tom Englund
427332a7ca memory: use initializer list in constructors
for trivial types this is optimized away, non trivial it default
constructs and then assigns it upon construction, minor waste. so lets
use initializer list for these custom types.
2025-09-27 01:01:56 +02:00
61e295340d
region: avoid tons of allocations for scale()
pixman_region32_rectangles returns the actual rects, so we shouldnt waste cycles when scaling em
2025-09-26 23:17:45 +01:00
73 changed files with 3674 additions and 1095 deletions

View file

@ -17,7 +17,7 @@ jobs:
run: | run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman pacman --noconfirm --noprogressbar -Sy gcc gtest base-devel cmake clang libc++ pixman
- name: Build hyprutils with gcc - name: Build hyprutils with gcc
run: | run: |
@ -44,7 +44,7 @@ jobs:
run: | run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman pacman --noconfirm --noprogressbar -Sy gcc gtest base-devel cmake clang libc++ pixman
- name: Build hyprutils with clang - name: Build hyprutils with clang
run: | run: |

View file

@ -1,6 +1,7 @@
name: Build & Test name: Build & Test
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
jobs: jobs:
nix: nix:
strategy: strategy:
@ -9,46 +10,8 @@ jobs:
- hyprutils - hyprutils
- hyprutils-with-tests - hyprutils-with-tests
runs-on: ubuntu-latest if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
steps: uses: hyprwm/actions/.github/workflows/nix.yml@main
- uses: actions/checkout@v3 secrets: inherit
with:
- name: Install Nix command: nix build .#${{ matrix.package }} --print-build-logs
uses: nixbuild/nix-quick-install-action@v31
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6
with:
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}-
# collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache
# 1G = 1073741824
gc-max-store-size-linux: 1G
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}-
# created more than this number of seconds ago
purge-created: 0
# or, last accessed more than this number of seconds ago
# relative to the start of the `Post Restore and save Nix store` phase
purge-last-accessed: 0
# except any version with the key that is the same as the `primary-key`
purge-primary-key: never
# not needed (yet)
# - uses: cachix/cachix-action@v12
# with:
# name: hyprland
# authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build & Test
run: nix build .#${{ matrix.package }} --print-build-logs

View file

@ -18,7 +18,7 @@ set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR})
configure_file(hyprutils.pc.in hyprutils.pc @ONLY) configure_file(hyprutils.pc.in hyprutils.pc @ONLY)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 26)
add_compile_options( add_compile_options(
-Wall -Wall
-Wextra -Wextra
@ -33,9 +33,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring hyprutils in Debug") message(STATUS "Configuring hyprutils in Debug")
add_compile_definitions(HYPRLAND_DEBUG) add_compile_definitions(HYPRLAND_DEBUG)
set(BUILD_TESTING ON)
else() else()
add_compile_options(-O3) add_compile_options(-O3)
message(STATUS "Configuring hyprutils in Release") message(STATUS "Configuring hyprutils in Release")
set(BUILD_TESTING OFF)
endif() endif()
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/*.hpp") file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/*.hpp")
@ -50,67 +52,33 @@ target_include_directories(
PUBLIC "./include" PUBLIC "./include"
PRIVATE "./src") PRIVATE "./src")
set_target_properties(hyprutils PROPERTIES VERSION ${hyprutils_VERSION} set_target_properties(hyprutils PROPERTIES VERSION ${hyprutils_VERSION}
SOVERSION 7) SOVERSION 12)
target_link_libraries(hyprutils PkgConfig::deps) target_link_libraries(hyprutils PkgConfig::deps)
# tests if(BUILD_TESTING)
add_custom_target(tests) # GTest
find_package(GTest CONFIG REQUIRED)
include(GoogleTest)
file(GLOB_RECURSE TESTFILES CONFIGURE_DEPENDS "tests/*.cpp")
add_executable(hyprutils_tests ${TESTFILES})
add_executable(hyprutils_memory "tests/memory.cpp") target_compile_options(hyprutils_tests PRIVATE --coverage -fsanitize=address)
target_link_libraries(hyprutils_memory PRIVATE hyprutils PkgConfig::deps) target_link_options(hyprutils_tests PRIVATE --coverage)
add_test(
NAME "Memory"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_memory "memory")
add_dependencies(tests hyprutils_memory)
add_executable(hyprutils_string "tests/string.cpp") target_include_directories(
target_link_libraries(hyprutils_string PRIVATE hyprutils PkgConfig::deps) hyprutils_tests
add_test( PUBLIC "./include"
NAME "String" PRIVATE "./src" "./src/include" "./protocols" "${CMAKE_BINARY_DIR}")
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests target_link_libraries(hyprutils_tests PRIVATE asan hyprutils GTest::gtest_main
COMMAND hyprutils_string "string") PkgConfig::deps)
add_dependencies(tests hyprutils_string) gtest_discover_tests(hyprutils_tests
PROPERTIES ENVIRONMENT "ASAN_OPTIONS=detect_leaks=0"
)
add_executable(hyprutils_signal "tests/signal.cpp") # Add coverage to hyprutils for test builds
target_link_libraries(hyprutils_signal PRIVATE hyprutils PkgConfig::deps) target_compile_options(hyprutils PRIVATE --coverage)
add_test( target_link_options(hyprutils PRIVATE --coverage)
NAME "Signal" endif()
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_signal "signal")
add_dependencies(tests hyprutils_signal)
add_executable(hyprutils_math "tests/math.cpp")
target_link_libraries(hyprutils_math PRIVATE hyprutils PkgConfig::deps)
add_test(
NAME "Math"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_math "math")
add_dependencies(tests hyprutils_math)
add_executable(hyprutils_os "tests/os.cpp")
target_link_libraries(hyprutils_os PRIVATE hyprutils PkgConfig::deps)
add_test(
NAME "OS"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_os "os")
add_dependencies(tests hyprutils_os)
add_executable(hyprutils_filedescriptor "tests/filedescriptor.cpp")
target_link_libraries(hyprutils_filedescriptor PRIVATE hyprutils PkgConfig::deps)
add_test(
NAME "Filedescriptor"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_filedescriptor "filedescriptor")
add_dependencies(tests hyprutils_filedescriptor)
add_executable(hyprutils_animation "tests/animation.cpp")
target_link_libraries(hyprutils_animation PRIVATE hyprutils PkgConfig::deps)
add_test(
NAME "Animation"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
COMMAND hyprutils_animation "utils")
add_dependencies(tests hyprutils_animation)
# Installation # Installation
install(TARGETS hyprutils) install(TARGETS hyprutils)

View file

@ -1 +1 @@
0.8.4 0.13.1

View file

@ -6,26 +6,31 @@
systems.url = "github:nix-systems/default-linux"; systems.url = "github:nix-systems/default-linux";
}; };
outputs = { outputs =
self, {
nixpkgs, self,
systems, nixpkgs,
}: let systems,
inherit (nixpkgs) lib; }:
eachSystem = lib.genAttrs (import systems); let
pkgsFor = eachSystem (system: inherit (nixpkgs) lib;
import nixpkgs { eachSystem = lib.genAttrs (import systems);
localSystem.system = system; pkgsFor = eachSystem (
overlays = with self.overlays; [hyprutils]; system:
import nixpkgs {
localSystem.system = system;
overlays = with self.overlays; [ hyprutils ];
}
);
in
{
overlays = import ./nix/overlays.nix { inherit self lib; };
packages = eachSystem (system: {
default = self.packages.${system}.hyprutils;
inherit (pkgsFor.${system}) hyprutils hyprutils-debug hyprutils-with-tests;
}); });
in {
overlays = import ./nix/overlays.nix {inherit self lib;};
packages = eachSystem (system: { formatter = eachSystem (system: pkgsFor.${system}.nixfmt-tree);
default = self.packages.${system}.hyprutils; };
inherit (pkgsFor.${system}) hyprutils hyprutils-debug hyprutils-with-tests;
});
formatter = eachSystem (system: pkgsFor.${system}.alejandra);
};
} }

View file

@ -8,6 +8,10 @@
#include <functional> #include <functional>
#include <chrono> #include <chrono>
#include <cmath>
#include <concepts>
#include <string_view>
#include <type_traits>
namespace Hyprutils { namespace Hyprutils {
namespace Animation { namespace Animation {
@ -17,11 +21,17 @@ namespace Hyprutils {
public: public:
using CallbackFun = std::function<void(Memory::CWeakPointer<CBaseAnimatedVariable> thisptr)>; using CallbackFun = std::function<void(Memory::CWeakPointer<CBaseAnimatedVariable> thisptr)>;
struct SCurveStepResult {
float value = 1.f;
bool finished = true;
};
CBaseAnimatedVariable() { CBaseAnimatedVariable() {
; // m_bDummy = true; ; // m_bDummy = true;
}; };
void create(CAnimationManager*, int, Memory::CSharedPointer<CBaseAnimatedVariable>); void create(CAnimationManager*, int, Memory::CSharedPointer<CBaseAnimatedVariable>);
void create2(CAnimationManager*, int, Memory::CWeakPointer<CBaseAnimatedVariable>);
void connectToActive(); void connectToActive();
void disconnectFromActive(); void disconnectFromActive();
@ -56,6 +66,11 @@ namespace Hyprutils {
/* returns the current curve value. */ /* returns the current curve value. */
float getCurveValue() const; float getCurveValue() const;
/* steps the current curve by one frame. */
SCurveStepResult getCurveStep();
bool isSpringCurve() const;
/* checks if an animation is in progress */ /* checks if an animation is in progress */
bool isBeingAnimated() const { bool isBeingAnimated() const {
return m_bIsBeingAnimated; return m_bIsBeingAnimated;
@ -83,7 +98,7 @@ namespace Hyprutils {
void resetAllCallbacks(); void resetAllCallbacks();
void onAnimationEnd(); void onAnimationEnd();
void onAnimationBegin(); void onAnimationBegin(bool preserveCurveState = false, float springVelocityScale = 1.F);
/* returns whether the parent CAnimationManager is dead */ /* returns whether the parent CAnimationManager is dead */
bool isAnimationManagerDead() const; bool isAnimationManagerDead() const;
@ -103,12 +118,19 @@ namespace Hyprutils {
Memory::CWeakPointer<CAnimationManager::SAnimationManagerSignals> m_pSignals; Memory::CWeakPointer<CAnimationManager::SAnimationManagerSignals> m_pSignals;
private: private:
void resetSpringState(bool preserveVelocity, float velocityScale);
std::string_view springNameFromSpec(const std::string& spec) const;
Memory::CWeakPointer<SAnimationPropertyConfig> m_pConfig; Memory::CWeakPointer<SAnimationPropertyConfig> m_pConfig;
std::chrono::steady_clock::time_point animationBegin; std::chrono::steady_clock::time_point animationBegin;
std::chrono::steady_clock::time_point springLastStep;
bool m_bDummy = true; bool m_bDummy = true;
float m_fSpringValue = 1.F;
float m_fSpringVelocity = 0.F;
bool m_bRemoveEndAfterRan = true; bool m_bRemoveEndAfterRan = true;
bool m_bRemoveBeginAfterRan = true; bool m_bRemoveBeginAfterRan = true;
@ -125,6 +147,12 @@ namespace Hyprutils {
{ val = val }; // requires operator= { val = val }; // requires operator=
}; };
template <class ValueImpl>
concept AnimableType = AnimatedType<ValueImpl> && requires(ValueImpl val, float pointy) {
{ val - val };
{ val + ((val - val) * pointy) } -> std::convertible_to<ValueImpl>;
};
/* /*
A generic class for variables. A generic class for variables.
VarType is the type of the variable to be animated. VarType is the type of the variable to be animated.
@ -136,6 +164,7 @@ namespace Hyprutils {
public: public:
CGenericAnimatedVariable() = default; CGenericAnimatedVariable() = default;
/* Deprecated: use create2 */
void create(const int typeInfo, CAnimationManager* pAnimationManager, Memory::CSharedPointer<CGenericAnimatedVariable<VarType, AnimationContext>> pSelf, void create(const int typeInfo, CAnimationManager* pAnimationManager, Memory::CSharedPointer<CGenericAnimatedVariable<VarType, AnimationContext>> pSelf,
const VarType& initialValue) { const VarType& initialValue) {
m_Begun = initialValue; m_Begun = initialValue;
@ -145,6 +174,16 @@ namespace Hyprutils {
CBaseAnimatedVariable::create(pAnimationManager, typeInfo, pSelf); CBaseAnimatedVariable::create(pAnimationManager, typeInfo, pSelf);
} }
/* Equivalent to create, except that it allows animated variables to be UP's */
void create2(const int typeInfo, CAnimationManager* pAnimationManager, Memory::CWeakPointer<CGenericAnimatedVariable<VarType, AnimationContext>> pSelf,
const VarType& initialValue) {
m_Begun = initialValue;
m_Value = initialValue;
m_Goal = initialValue;
CBaseAnimatedVariable::create2(pAnimationManager, typeInfo, pSelf);
}
CGenericAnimatedVariable(const CGenericAnimatedVariable&) = delete; CGenericAnimatedVariable(const CGenericAnimatedVariable&) = delete;
CGenericAnimatedVariable(CGenericAnimatedVariable&&) = delete; CGenericAnimatedVariable(CGenericAnimatedVariable&&) = delete;
CGenericAnimatedVariable& operator=(const CGenericAnimatedVariable&) = delete; CGenericAnimatedVariable& operator=(const CGenericAnimatedVariable&) = delete;
@ -188,10 +227,25 @@ namespace Hyprutils {
if (v == m_Goal) if (v == m_Goal)
return *this; return *this;
const bool WASANIMATING = m_bIsBeingAnimated;
float SPRINGVELOCITYSCALE = 1.f;
if (WASANIMATING && isSpringCurve()) {
if constexpr (std::is_arithmetic_v<VarType>) {
const float OLDDELTA = static_cast<float>(m_Goal - m_Begun);
const float NEWDELTA = static_cast<float>(v - m_Value);
if (std::abs(NEWDELTA) > 1e-6f)
SPRINGVELOCITYSCALE = OLDDELTA / NEWDELTA;
else
SPRINGVELOCITYSCALE = 0.f;
}
}
m_Goal = v; m_Goal = v;
m_Begun = m_Value; m_Begun = m_Value;
onAnimationBegin(); onAnimationBegin(WASANIMATING && isSpringCurve(), SPRINGVELOCITYSCALE);
return *this; return *this;
} }
@ -215,6 +269,28 @@ namespace Hyprutils {
warp(); warp();
} }
template <class T = VarType>
requires AnimableType<T>
SCurveStepResult update(bool warpNow = false) {
if (warpNow || m_Value == m_Goal || !enabled()) {
warp(true, false);
return SCurveStepResult{.value = 1.F, .finished = true};
}
const auto STEP = getCurveStep();
if (STEP.finished) {
warp(true, false);
return STEP;
}
const auto DELTA = m_Goal - m_Begun;
m_Value = m_Begun + (DELTA * STEP.value);
onUpdate();
return STEP;
}
AnimationContext m_Context; AnimationContext m_Context;
private: private:

View file

@ -13,6 +13,14 @@ namespace Hyprutils {
namespace Animation { namespace Animation {
class CBaseAnimatedVariable; class CBaseAnimatedVariable;
struct SSpringCurve {
float stiffness = 250.F;
float damping = 25.F;
float mass = 1.F;
float valueEpsilon = 0.001F;
float velocityEpsilon = 0.001F;
};
/* A class for managing bezier curves and variables that are being animated. */ /* A class for managing bezier curves and variables that are being animated. */
class CAnimationManager { class CAnimationManager {
public: public:
@ -28,11 +36,16 @@ namespace Hyprutils {
void addBezierWithName(std::string, const Math::Vector2D&, const Math::Vector2D&); void addBezierWithName(std::string, const Math::Vector2D&, const Math::Vector2D&);
void removeAllBeziers(); void removeAllBeziers();
void addSpringWithName(std::string, const SSpringCurve&);
void removeAllSprings();
bool bezierExists(const std::string&); bool bezierExists(const std::string&);
bool springExists(const std::string&);
Memory::CSharedPointer<CBezierCurve> getBezier(const std::string&); Memory::CSharedPointer<CBezierCurve> getBezier(const std::string&);
Memory::CSharedPointer<SSpringCurve> getSpring(const std::string&);
const std::unordered_map<std::string, Memory::CSharedPointer<CBezierCurve>>& getAllBeziers(); const std::unordered_map<std::string, Memory::CSharedPointer<CBezierCurve>>& getAllBeziers();
const std::unordered_map<std::string, Memory::CSharedPointer<SSpringCurve>>& getAllSprings();
struct SAnimationManagerSignals { struct SAnimationManagerSignals {
Signal::CSignalT<Memory::CWeakPointer<CBaseAnimatedVariable>> connect; Signal::CSignalT<Memory::CWeakPointer<CBaseAnimatedVariable>> connect;
@ -45,6 +58,7 @@ namespace Hyprutils {
private: private:
std::unordered_map<std::string, Memory::CSharedPointer<CBezierCurve>> m_mBezierCurves; std::unordered_map<std::string, Memory::CSharedPointer<CBezierCurve>> m_mBezierCurves;
std::unordered_map<std::string, Memory::CSharedPointer<SSpringCurve>> m_mSpringCurves;
bool m_bTickScheduled = false; bool m_bTickScheduled = false;

View file

@ -14,7 +14,9 @@ namespace Hyprutils {
class CBezierCurve { class CBezierCurve {
public: public:
/* Calculates a cubic bezier curve based on 2 control points (EXCLUDES the 0,0 and 1,1 points). */ /* Calculates a cubic bezier curve based on 2 control points (EXCLUDES the 0,0 and 1,1 points). */
void setup(const std::array<Hyprutils::Math::Vector2D, 2>& points); void setup(const std::array<Hyprutils::Math::Vector2D, 2>& points);
/* Calculates a cubic bezier curve based on 4 control points. */
void setup4(const std::array<Hyprutils::Math::Vector2D, 4>& points);
float getYForT(float const& t) const; float getYForT(float const& t) const;
float getXForT(float const& t) const; float getXForT(float const& t) const;

View file

@ -0,0 +1,36 @@
#pragma once
#include <span>
#include <string>
#include <string_view>
#include <optional>
#include <expected>
#include "../memory/UniquePtr.hpp"
namespace Hyprutils::CLI {
class CArgumentParserImpl;
class CArgumentParser {
public:
CArgumentParser(const std::span<const char*>& args);
~CArgumentParser() = default;
std::expected<void, std::string> registerBoolOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description);
std::expected<void, std::string> registerIntOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description);
std::expected<void, std::string> registerFloatOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description);
std::expected<void, std::string> registerStringOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description);
std::optional<bool> getBool(const std::string_view& name);
std::optional<int> getInt(const std::string_view& name);
std::optional<float> getFloat(const std::string_view& name);
std::optional<std::string_view> getString(const std::string_view& name);
// commence the parsing after registering
std::expected<void, std::string> parse();
std::string getDescription(const std::string_view& header, std::optional<size_t> maxWidth = {});
private:
Memory::CUniquePointer<CArgumentParserImpl> m_impl;
};
};

View file

@ -0,0 +1,116 @@
#pragma once
#include <format>
#include <expected>
#include <string_view>
#include "../memory/UniquePtr.hpp"
#include "../memory/WeakPtr.hpp"
namespace Hyprutils::CLI {
class CLoggerImpl;
enum eLogLevel : uint8_t {
LOG_TRACE = 0,
LOG_DEBUG,
LOG_WARN,
LOG_ERR,
LOG_CRIT,
};
// CLogger is a thread-safe, general purpose logger.
// the logger's stdout is enabled by default.
// color is enabled by default, it's only for stdout.
// everything else is disabled.
class CLogger {
public:
CLogger();
~CLogger();
CLogger(const CLogger&) = delete;
CLogger(CLogger&) = delete;
CLogger(CLogger&&) = delete;
void setLogLevel(eLogLevel level);
void setTime(bool enabled);
void setEnableStdout(bool enabled);
void setEnableColor(bool enabled);
void setEnableRolling(bool enabled);
std::expected<void, std::string> setOutputFile(const std::string_view& file);
const std::string& rollingLog();
void log(eLogLevel level, const std::string_view& msg);
template <typename... Args>
// NOLINTNEXTLINE
void log(eLogLevel level, std::format_string<Args...> fmt, Args&&... args) {
if (!m_shouldLogAtAll)
return;
if (level < m_logLevel)
return;
std::string logMsg = std::vformat(fmt.get(), std::make_format_args(args...));
log(level, logMsg);
}
private:
Memory::CUniquePointer<CLoggerImpl> m_impl;
// this has to be here as part of important optimization of trace logs
eLogLevel m_logLevel = LOG_DEBUG;
// this has to be here as part of important optimization of disabled logging
bool m_shouldLogAtAll = false;
friend class CLoggerImpl;
friend class CLoggerConnection;
};
// CLoggerConnection is a "handle" to a logger, that can be created from a logger and
// allows to send messages to a logger via a proxy
// this does not allow for any changes to the logger itself, only sending logs.
// Logger connections keep their own logLevel. They inherit it at creation, but can be changed
class CLoggerConnection {
public:
CLoggerConnection(CLogger& logger);
~CLoggerConnection();
CLoggerConnection(const CLoggerConnection&) = delete;
CLoggerConnection(CLoggerConnection&) = delete;
// Allow move
CLoggerConnection(CLoggerConnection&&) = default;
void setName(const std::string_view& name);
void setLogLevel(eLogLevel level);
void log(eLogLevel level, const std::string_view& msg);
CLogger* getLogger();
void redirect(CLogger& logger);
template <typename... Args>
// NOLINTNEXTLINE
void log(eLogLevel level, std::format_string<Args...> fmt, Args&&... args) {
if (!m_impl || !m_logger)
return;
if (!m_logger->m_shouldLogAtAll)
return;
if (level < m_logLevel)
return;
std::string logMsg = std::vformat(fmt.get(), std::make_format_args(args...));
log(level, logMsg);
}
private:
Memory::CWeakPointer<CLoggerImpl> m_impl;
CLogger* m_logger = nullptr;
eLogLevel m_logLevel = LOG_DEBUG;
std::string m_name = "";
};
};

View file

@ -0,0 +1,55 @@
#pragma once
#include "../memory/UniquePtr.hpp"
#include <cstdint>
#include <string>
#include <unordered_map>
#include <functional>
namespace Hyprutils::I18n {
struct SI18nEngineImpl;
typedef std::unordered_map<std::string, std::string> translationVarMap;
typedef std::function<std::string(const translationVarMap&)> translationFn;
class CI18nLocale {
public:
~CI18nLocale() = default;
std::string locale();
std::string stem();
std::string full();
private:
CI18nLocale(std::string fullLocale);
std::string m_locale, m_rawFullLocale;
friend class CI18nEngine;
};
class CI18nEngine {
public:
CI18nEngine();
~CI18nEngine();
/*
Register a translation entry. The internal translation db is kept as a vector,
so make sure your keys are linear, don't use e.g. 2 billion as that will call
.resize() on the vec to 2 billion.
If you pass a Fn, you can do logic, e.g. "1 point" vs "2 points".
*/
void registerEntry(const std::string& locale, uint64_t key, std::string&& translationUTF8);
void registerEntry(const std::string& locale, uint64_t key, translationFn&& translationFn);
void setFallbackLocale(const std::string& locale);
std::string localizeEntry(const std::string& locale, uint64_t key, const translationVarMap& map);
CI18nLocale getSystemLocale();
private:
Memory::CUniquePointer<SI18nEngineImpl> m_impl;
};
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <hyprutils/memory/Casts.hpp>
#include <hyprutils/math/Misc.hpp> #include <hyprutils/math/Misc.hpp>
#include <format> #include <format>
@ -9,82 +10,86 @@ namespace Hyprutils {
namespace Math { namespace Math {
class Vector2D { class Vector2D {
public: public:
Vector2D(double, double); constexpr Vector2D(double xx, double yy) : x(xx), y(yy) {
Vector2D(int, int); ;
Vector2D(); }
~Vector2D(); constexpr Vector2D(int xx, int yy) : x(Hyprutils::Memory::sc<double>(xx)), y(Hyprutils::Memory::sc<double>(yy)) {
;
}
constexpr Vector2D() = default;
~Vector2D() = default;
double x = 0; double x = 0;
double y = 0; double y = 0;
// returns the scale // returns the scale
double normalize(); double normalize();
Vector2D operator+(const Vector2D& a) const { constexpr Vector2D operator+(const Vector2D& a) const {
return Vector2D(this->x + a.x, this->y + a.y); return Vector2D(this->x + a.x, this->y + a.y);
} }
Vector2D operator-(const Vector2D& a) const { constexpr Vector2D operator-(const Vector2D& a) const {
return Vector2D(this->x - a.x, this->y - a.y); return Vector2D(this->x - a.x, this->y - a.y);
} }
Vector2D operator-() const { constexpr Vector2D operator-() const {
return Vector2D(-this->x, -this->y); return Vector2D(-this->x, -this->y);
} }
Vector2D operator*(const double& a) const { constexpr Vector2D operator*(const double& a) const {
return Vector2D(this->x * a, this->y * a); return Vector2D(this->x * a, this->y * a);
} }
Vector2D operator/(const double& a) const { constexpr Vector2D operator/(const double& a) const {
return Vector2D(this->x / a, this->y / a); return Vector2D(this->x / a, this->y / a);
} }
bool operator==(const Vector2D& a) const { constexpr bool operator==(const Vector2D& a) const {
return a.x == x && a.y == y; return a.x == x && a.y == y;
} }
bool operator!=(const Vector2D& a) const { constexpr bool operator!=(const Vector2D& a) const {
return a.x != x || a.y != y; return a.x != x || a.y != y;
} }
Vector2D operator*(const Vector2D& a) const { constexpr Vector2D operator*(const Vector2D& a) const {
return Vector2D(this->x * a.x, this->y * a.y); return Vector2D(this->x * a.x, this->y * a.y);
} }
Vector2D operator/(const Vector2D& a) const { constexpr Vector2D operator/(const Vector2D& a) const {
return Vector2D(this->x / a.x, this->y / a.y); return Vector2D(this->x / a.x, this->y / a.y);
} }
bool operator>(const Vector2D& a) const { constexpr bool operator>(const Vector2D& a) const {
return this->x > a.x && this->y > a.y; return this->x > a.x && this->y > a.y;
} }
bool operator<(const Vector2D& a) const { constexpr bool operator<(const Vector2D& a) const {
return this->x < a.x && this->y < a.y; return this->x < a.x && this->y < a.y;
} }
Vector2D& operator+=(const Vector2D& a) { constexpr Vector2D& operator+=(const Vector2D& a) {
this->x += a.x; this->x += a.x;
this->y += a.y; this->y += a.y;
return *this; return *this;
} }
Vector2D& operator-=(const Vector2D& a) { constexpr Vector2D& operator-=(const Vector2D& a) {
this->x -= a.x; this->x -= a.x;
this->y -= a.y; this->y -= a.y;
return *this; return *this;
} }
Vector2D& operator*=(const Vector2D& a) { constexpr Vector2D& operator*=(const Vector2D& a) {
this->x *= a.x; this->x *= a.x;
this->y *= a.y; this->y *= a.y;
return *this; return *this;
} }
Vector2D& operator/=(const Vector2D& a) { constexpr Vector2D& operator/=(const Vector2D& a) {
this->x /= a.x; this->x /= a.x;
this->y /= a.y; this->y /= a.y;
return *this; return *this;
} }
Vector2D& operator*=(const double& a) { constexpr Vector2D& operator*=(const double& a) {
this->x *= a; this->x *= a;
this->y *= a; this->y *= a;
return *this; return *this;
} }
Vector2D& operator/=(const double& a) { constexpr Vector2D& operator/=(const double& a) {
this->x /= a; this->x /= a;
this->y /= a; this->y /= a;
return *this; return *this;

View file

@ -23,12 +23,11 @@
namespace Hyprutils::Memory { namespace Hyprutils::Memory {
namespace Atomic_ { namespace Atomic_ {
template <typename T> class impl : public Impl_::impl_base {
class impl : public Impl_::impl<T> {
std::recursive_mutex m_mutex; std::recursive_mutex m_mutex;
public: public:
impl(T* data, bool lock = true) noexcept : Impl_::impl<T>(data, lock) { impl(void* data, DeleteFn deleter) noexcept : Impl_::impl_base(data, deleter) {
; ;
} }
@ -50,12 +49,18 @@ namespace Hyprutils::Memory {
template <typename T> template <typename T>
class CAtomicSharedPointer { class CAtomicSharedPointer {
template <typename X> template <typename X>
using isConstructible = typename std::enable_if<std::is_constructible<T&, X&>::value>::type; using isConstructible = std::enable_if_t<std::is_constructible_v<T&, X&>>;
template <typename X> template <typename X>
using validHierarchy = typename std::enable_if<std::is_assignable<CAtomicSharedPointer<T>&, X>::value, CAtomicSharedPointer&>::type; using validHierarchy = std::enable_if_t<std::is_assignable_v<CAtomicSharedPointer<T>&, X>, CAtomicSharedPointer&>;
public: public:
explicit CAtomicSharedPointer(Impl_::impl_base* impl) noexcept : m_ptr(impl) {} explicit CAtomicSharedPointer(T* object) noexcept : m_ptr(new Atomic_::impl(sc<void*>(object), _delete), sc<void*>(object)) {
;
}
CAtomicSharedPointer(Impl_::impl_base* impl, void* data) noexcept : m_ptr(impl, data) {
;
}
CAtomicSharedPointer(const CAtomicSharedPointer<T>& ref) { CAtomicSharedPointer(const CAtomicSharedPointer<T>& ref) {
if (!ref.m_ptr.impl_) if (!ref.m_ptr.impl_)
@ -141,7 +146,7 @@ namespace Hyprutils::Memory {
// -> must unlock BEFORE reset // -> must unlock BEFORE reset
// not last ref? // not last ref?
// -> must unlock AFTER reset // -> must unlock AFTER reset
auto& mutex = sc<Atomic_::impl<T>*>(m_ptr.impl_)->getMutex(); auto& mutex = sc<Atomic_::impl*>(m_ptr.impl_)->getMutex();
mutex.lock(); mutex.lock();
if (m_ptr.impl_->ref() > 1) { if (m_ptr.impl_->ref() > 1) {
@ -152,7 +157,11 @@ namespace Hyprutils::Memory {
if (m_ptr.impl_->wref() == 0) { if (m_ptr.impl_->wref() == 0) {
mutex.unlock(); // Don't hold the mutex when destroying it mutex.unlock(); // Don't hold the mutex when destroying it
m_ptr.reset();
m_ptr.impl_->destroy();
delete sc<Atomic_::impl*>(m_ptr.impl_);
m_ptr.impl_ = nullptr;
// mutex invalid // mutex invalid
return; return;
} else { } else {
@ -163,12 +172,18 @@ namespace Hyprutils::Memory {
// To avoid this altogether, keep a weak pointer here. // To avoid this altogether, keep a weak pointer here.
// This guarantees that impl_ is still valid after the reset. // This guarantees that impl_ is still valid after the reset.
CWeakPointer<T> guard = m_ptr; CWeakPointer<T> guard = m_ptr;
m_ptr.reset(); m_ptr.reset(); // destroys the data
// Now we can safely check if guard is the last wref. // Now we can safely check if guard is the last wref.
if (guard.impl_->wref() == 1) { if (guard.impl_->wref() == 1) {
mutex.unlock(); mutex.unlock();
return; // ~guard destroys impl_ and mutex
// destroy impl_ (includes the mutex)
delete sc<Atomic_::impl*>(guard.impl_);
guard.impl_ = nullptr;
// mutex invalid
return;
} }
guard.reset(); guard.reset();
@ -204,9 +219,17 @@ namespace Hyprutils::Memory {
return m_ptr.impl_ ? m_ptr.impl_->ref() : 0; return m_ptr.impl_ ? m_ptr.impl_->ref() : 0;
} }
Atomic_::impl* impl() const {
return sc<Atomic_::impl*>(m_ptr.impl_);
}
private: private:
static void _delete(void* p) {
std::default_delete<T>{}(sc<T*>(p));
}
std::lock_guard<std::recursive_mutex> implLockGuard() const { std::lock_guard<std::recursive_mutex> implLockGuard() const {
return sc<Atomic_::impl<T>*>(m_ptr.impl_)->lockGuard(); return impl()->lockGuard();
} }
CSharedPointer<T> m_ptr; CSharedPointer<T> m_ptr;
@ -221,9 +244,9 @@ namespace Hyprutils::Memory {
class CAtomicWeakPointer { class CAtomicWeakPointer {
template <typename X> template <typename X>
using isConstructible = typename std::enable_if<std::is_constructible<T&, X&>::value>::type; using isConstructible = std::enable_if_t<std::is_constructible_v<T&, X&>>;
template <typename X> template <typename X>
using validHierarchy = typename std::enable_if<std::is_assignable<CAtomicWeakPointer<T>&, X>::value, CAtomicWeakPointer&>::type; using validHierarchy = std::enable_if_t<std::is_assignable_v<CAtomicWeakPointer<T>&, X>, CAtomicWeakPointer&>;
public: public:
CAtomicWeakPointer(const CAtomicWeakPointer<T>& ref) { CAtomicWeakPointer(const CAtomicWeakPointer<T>& ref) {
@ -262,6 +285,10 @@ namespace Hyprutils::Memory {
CAtomicWeakPointer() noexcept = default; CAtomicWeakPointer() noexcept = default;
CAtomicWeakPointer(Impl_::impl_base* implementation, void* data) noexcept : m_ptr(implementation, data) {
;
}
CAtomicWeakPointer(std::nullptr_t) noexcept { CAtomicWeakPointer(std::nullptr_t) noexcept {
; // empty ; // empty
} }
@ -312,11 +339,13 @@ namespace Hyprutils::Memory {
// -> must unlock BEFORE reset // -> must unlock BEFORE reset
// not last ref? // not last ref?
// -> must unlock AFTER reset // -> must unlock AFTER reset
auto& mutex = sc<Atomic_::impl<T>*>(m_ptr.impl_)->getMutex(); auto& mutex = sc<Atomic_::impl*>(m_ptr.impl_)->getMutex();
mutex.lock(); mutex.lock();
if (m_ptr.impl_->ref() == 0 && m_ptr.impl_->wref() == 1) { if (m_ptr.impl_->ref() == 0 && m_ptr.impl_->wref() == 1) {
mutex.unlock(); mutex.unlock();
m_ptr.reset();
delete sc<Atomic_::impl*>(m_ptr.impl_);
m_ptr.impl_ = nullptr;
// mutex invalid // mutex invalid
return; return;
} }
@ -370,12 +399,16 @@ namespace Hyprutils::Memory {
if (!m_ptr.impl_->dataNonNull() || m_ptr.impl_->destroying() || !m_ptr.impl_->lockable()) if (!m_ptr.impl_->dataNonNull() || m_ptr.impl_->destroying() || !m_ptr.impl_->lockable())
return {}; return {};
return CAtomicSharedPointer<T>(m_ptr.impl_); return CAtomicSharedPointer<T>(m_ptr.impl_, m_ptr.m_data);
}
Atomic_::impl* impl() const {
return sc<Atomic_::impl*>(m_ptr.impl_);
} }
private: private:
std::lock_guard<std::recursive_mutex> implLockGuard() const { std::lock_guard<std::recursive_mutex> implLockGuard() const {
return sc<Atomic_::impl<T>*>(m_ptr.impl_)->lockGuard(); return impl()->lockGuard();
} }
CWeakPointer<T> m_ptr; CWeakPointer<T> m_ptr;
@ -387,7 +420,32 @@ namespace Hyprutils::Memory {
}; };
template <typename U, typename... Args> template <typename U, typename... Args>
static CAtomicSharedPointer<U> makeAtomicShared(Args&&... args) { [[nodiscard]] inline CAtomicSharedPointer<U> makeAtomicShared(Args&&... args) {
return CAtomicSharedPointer<U>(new Atomic_::impl<U>(new U(std::forward<Args>(args)...))); return CAtomicSharedPointer<U>(new U(std::forward<Args>(args)...));
}
template <typename T, typename U>
CAtomicSharedPointer<T> reinterpretPointerCast(const CAtomicSharedPointer<U>& ref) {
return CAtomicSharedPointer<T>(ref.impl(), ref.m_data);
}
template <typename T, typename U>
CAtomicSharedPointer<T> dynamicPointerCast(const CAtomicSharedPointer<U>& ref) {
if (!ref)
return nullptr;
T* newPtr = dynamic_cast<T*>(sc<U*>(ref.impl()->getData()));
if (!newPtr)
return nullptr;
return CAtomicSharedPointer<T>(ref.impl(), newPtr);
}
template <typename T, typename U>
CAtomicWeakPointer<T> dynamicPointerCast(const CAtomicWeakPointer<U>& ref) {
if (!ref)
return nullptr;
T* newPtr = dynamic_cast<T*>(sc<U*>(ref.impl()->getData()));
if (!newPtr)
return nullptr;
return CAtomicWeakPointer<T>(ref.impl(), newPtr);
} }
} }

View file

@ -27,5 +27,3 @@ namespace Hyprutils::Memory {
return std::bit_cast<To>(from); return std::bit_cast<To>(from);
} }
} }

View file

@ -8,42 +8,69 @@ namespace Hyprutils {
namespace Impl_ { namespace Impl_ {
class impl_base { class impl_base {
public: public:
virtual ~impl_base() {}; using DeleteFn = void (*)(void*);
virtual void inc() noexcept = 0; impl_base(void* data, DeleteFn deleter, bool lock = true) noexcept : _lockable(lock), _data(data), _deleter(deleter) {
virtual void dec() noexcept = 0;
virtual void incWeak() noexcept = 0;
virtual void decWeak() noexcept = 0;
virtual unsigned int ref() noexcept = 0;
virtual unsigned int wref() noexcept = 0;
virtual void destroy() noexcept = 0;
virtual bool destroying() noexcept = 0;
virtual bool dataNonNull() noexcept = 0;
virtual bool lockable() noexcept = 0;
virtual void* getData() noexcept = 0;
};
template <typename T>
class impl : public impl_base {
public:
impl(T* data, bool lock = true) noexcept : _lockable(lock), _data(data) {
; ;
} }
void inc() noexcept {
_ref++;
}
void dec() noexcept {
_ref--;
}
void incWeak() noexcept {
_weak++;
}
void decWeak() noexcept {
_weak--;
}
unsigned int ref() noexcept {
return _ref;
}
unsigned int wref() noexcept {
return _weak;
}
void destroy() noexcept {
_destroy();
}
bool destroying() noexcept {
return _destroying;
}
bool lockable() noexcept {
return _lockable;
}
bool dataNonNull() noexcept {
return _data != nullptr;
}
void* getData() noexcept {
return _data;
}
~impl_base() {
destroy();
}
private:
/* strong refcount */ /* strong refcount */
unsigned int _ref = 0; unsigned int _ref = 0;
/* weak refcount */ /* weak refcount */
unsigned int _weak = 0; unsigned int _weak = 0;
/* if this is lockable (shared) */ /* if this is lockable (shared) */
bool _lockable = true; bool _lockable = true;
T* _data = nullptr; void* _data = nullptr;
friend void swap(impl*& a, impl*& b) {
impl* tmp = a;
a = b;
b = tmp;
}
/* if the destructor was called, /* if the destructor was called,
creating shared_ptrs is no longer valid */ creating shared_ptrs is no longer valid */
@ -57,62 +84,13 @@ namespace Hyprutils {
// this way, weak pointers will still be able to // this way, weak pointers will still be able to
// reference and use, but no longer create shared ones. // reference and use, but no longer create shared ones.
_destroying = true; _destroying = true;
__deleter(_data); _deleter(_data);
// now, we can reset the data and call it a day. // now, we can reset the data and call it a day.
_data = nullptr; _data = nullptr;
_destroying = false; _destroying = false;
} }
std::default_delete<T> __deleter{}; DeleteFn _deleter = nullptr;
//
virtual void inc() noexcept {
_ref++;
}
virtual void dec() noexcept {
_ref--;
}
virtual void incWeak() noexcept {
_weak++;
}
virtual void decWeak() noexcept {
_weak--;
}
virtual unsigned int ref() noexcept {
return _ref;
}
virtual unsigned int wref() noexcept {
return _weak;
}
virtual void destroy() noexcept {
_destroy();
}
virtual bool destroying() noexcept {
return _destroying;
}
virtual bool lockable() noexcept {
return _lockable;
}
virtual bool dataNonNull() noexcept {
return _data != nullptr;
}
virtual void* getData() noexcept {
return _data;
}
virtual ~impl() {
destroy();
}
}; };
} }
} }

View file

@ -22,41 +22,39 @@ namespace Hyprutils {
class CSharedPointer { class CSharedPointer {
public: public:
template <typename X> template <typename X>
using validHierarchy = typename std::enable_if<std::is_assignable<CSharedPointer<T>&, X>::value, CSharedPointer&>::type; using validHierarchy = std::enable_if_t<std::is_assignable_v<CSharedPointer<T>&, X>, CSharedPointer&>;
template <typename X> template <typename X>
using isConstructible = typename std::enable_if<std::is_constructible<T&, X&>::value>::type; using isConstructible = std::enable_if_t<std::is_constructible_v<T&, X&>>;
/* creates a new shared pointer managing a resource /* creates a new shared pointer managing a resource
avoid calling. Could duplicate ownership. Prefer makeShared */ avoid calling. Could duplicate ownership. Prefer makeShared */
explicit CSharedPointer(T* object) noexcept { explicit CSharedPointer(T* object) noexcept : impl_(new Impl_::impl_base(sc<void*>(object), _delete)), m_data(sc<void*>(object)) {
impl_ = new Impl_::impl<T>(object);
increment(); increment();
} }
/* creates a shared pointer from a reference */ /* creates a shared pointer from a reference */
template <typename U, typename = isConstructible<U>> template <typename U, typename = isConstructible<U>>
CSharedPointer(const CSharedPointer<U>& ref) noexcept { CSharedPointer(const CSharedPointer<U>& ref) noexcept : impl_(ref.impl_), m_data(ref.m_data) {
impl_ = ref.impl_;
increment(); increment();
} }
CSharedPointer(const CSharedPointer& ref) noexcept { CSharedPointer(const CSharedPointer& ref) noexcept : impl_(ref.impl_), m_data(ref.m_data) {
impl_ = ref.impl_;
increment(); increment();
} }
template <typename U, typename = isConstructible<U>> template <typename U, typename = isConstructible<U>>
CSharedPointer(CSharedPointer<U>&& ref) noexcept { CSharedPointer(CSharedPointer<U>&& ref) noexcept {
std::swap(impl_, ref.impl_); std::swap(impl_, ref.impl_);
std::swap(m_data, ref.m_data);
} }
CSharedPointer(CSharedPointer&& ref) noexcept { CSharedPointer(CSharedPointer&& ref) noexcept {
std::swap(impl_, ref.impl_); std::swap(impl_, ref.impl_);
std::swap(m_data, ref.m_data);
} }
/* allows weakPointer to create from an impl */ /* allows weakPointer to create from an impl */
CSharedPointer(Impl_::impl_base* implementation) noexcept { CSharedPointer(Impl_::impl_base* implementation, void* data) noexcept : impl_(implementation), m_data(data) {
impl_ = implementation;
increment(); increment();
} }
@ -69,7 +67,7 @@ namespace Hyprutils {
} }
~CSharedPointer() { ~CSharedPointer() {
decrement(); decrement(impl_);
} }
template <typename U> template <typename U>
@ -77,8 +75,9 @@ namespace Hyprutils {
if (impl_ == rhs.impl_) if (impl_ == rhs.impl_)
return *this; return *this;
decrement(); decrement(impl_);
impl_ = rhs.impl_; impl_ = rhs.impl_;
m_data = rhs.m_data;
increment(); increment();
return *this; return *this;
} }
@ -87,8 +86,9 @@ namespace Hyprutils {
if (impl_ == rhs.impl_) if (impl_ == rhs.impl_)
return *this; return *this;
decrement(); decrement(impl_);
impl_ = rhs.impl_; impl_ = rhs.impl_;
m_data = rhs.m_data;
increment(); increment();
return *this; return *this;
} }
@ -96,11 +96,13 @@ namespace Hyprutils {
template <typename U> template <typename U>
validHierarchy<const CSharedPointer<U>&> operator=(CSharedPointer<U>&& rhs) { validHierarchy<const CSharedPointer<U>&> operator=(CSharedPointer<U>&& rhs) {
std::swap(impl_, rhs.impl_); std::swap(impl_, rhs.impl_);
std::swap(m_data, rhs.m_data);
return *this; return *this;
} }
CSharedPointer& operator=(CSharedPointer&& rhs) { CSharedPointer& operator=(CSharedPointer&& rhs) noexcept {
std::swap(impl_, rhs.impl_); std::swap(impl_, rhs.impl_);
std::swap(m_data, rhs.m_data);
return *this; return *this;
} }
@ -108,6 +110,8 @@ namespace Hyprutils {
return impl_ && impl_->dataNonNull(); return impl_ && impl_->dataNonNull();
} }
// this compares that the pointed-to object is the same, but in multiple inheritance,
// different typed pointers can be equal if the object is the same
bool operator==(const CSharedPointer& rhs) const { bool operator==(const CSharedPointer& rhs) const {
return impl_ == rhs.impl_; return impl_ == rhs.impl_;
} }
@ -129,12 +133,14 @@ namespace Hyprutils {
} }
void reset() { void reset() {
decrement(); auto ptr = impl_;
impl_ = nullptr; impl_ = nullptr;
m_data = nullptr;
decrement(ptr);
} }
T* get() const { T* get() const {
return impl_ ? sc<T*>(impl_->getData()) : nullptr; return impl_ && impl_->dataNonNull() ? sc<T*>(m_data) : nullptr;
} }
unsigned int strongRef() const { unsigned int strongRef() const {
@ -143,21 +149,28 @@ namespace Hyprutils {
Impl_::impl_base* impl_ = nullptr; Impl_::impl_base* impl_ = nullptr;
// Never use directly: raw data ptr, could be UAF
void* m_data = nullptr;
private: private:
static void _delete(void* p) {
std::default_delete<T>{}(sc<T*>(p));
}
/* /*
no-op if there is no impl_ no-op if there is no impl_
may delete the stored object if ref == 0 may delete the stored object if ref == 0
may delete and reset impl_ if ref == 0 and weak == 0 may delete and reset impl_ if ref == 0 and weak == 0
*/ */
void decrement() { void decrement(Impl_::impl_base* base) {
if (!impl_) if (!base)
return; return;
impl_->dec(); base->dec();
// if ref == 0, we can destroy impl // if ref == 0, we can destroy impl
if (impl_->ref() == 0) if (base->ref() == 0)
destroyImpl(); destroyImpl(base);
} }
/* no-op if there is no impl_ */ /* no-op if there is no impl_ */
void increment() { void increment() {
@ -169,15 +182,13 @@ namespace Hyprutils {
/* destroy the pointed-to object /* destroy the pointed-to object
if able, will also destroy impl */ if able, will also destroy impl */
void destroyImpl() { void destroyImpl(Impl_::impl_base* base) {
// destroy the impl contents // this call can destroy this, so we need to not use thisptr anymore
impl_->destroy(); base->destroy();
// check for weak refs, if zero, we can also delete impl_ // check for weak refs, if zero, we can also delete base
if (impl_->wref() == 0) { if (base->wref() == 0)
delete impl_; delete base;
impl_ = nullptr;
}
} }
}; };
@ -188,7 +199,17 @@ namespace Hyprutils {
template <typename T, typename U> template <typename T, typename U>
CSharedPointer<T> reinterpretPointerCast(const CSharedPointer<U>& ref) { CSharedPointer<T> reinterpretPointerCast(const CSharedPointer<U>& ref) {
return CSharedPointer<T>(ref.impl_); return CSharedPointer<T>(ref.impl_, ref.m_data);
}
template <typename T, typename U>
CSharedPointer<T> dynamicPointerCast(const CSharedPointer<U>& ref) {
if (!ref)
return nullptr;
T* newPtr = dynamic_cast<T*>(sc<U*>(ref.impl_->getData()));
if (!newPtr)
return nullptr;
return CSharedPointer<T>(ref.impl_, newPtr);
} }
} }
} }

View file

@ -16,14 +16,13 @@ namespace Hyprutils {
class CUniquePointer { class CUniquePointer {
public: public:
template <typename X> template <typename X>
using validHierarchy = typename std::enable_if<std::is_assignable<CUniquePointer<T>&, X>::value, CUniquePointer&>::type; using validHierarchy = std::enable_if_t<std::is_assignable_v<CUniquePointer<T>&, X>, CUniquePointer&>;
template <typename X> template <typename X>
using isConstructible = typename std::enable_if<std::is_constructible<T&, X&>::value>::type; using isConstructible = std::enable_if_t<std::is_constructible_v<T&, X&>>;
/* creates a new unique pointer managing a resource /* creates a new unique pointer managing a resource
avoid calling. Could duplicate ownership. Prefer makeUnique */ avoid calling. Could duplicate ownership. Prefer makeUnique */
explicit CUniquePointer(T* object) noexcept { explicit CUniquePointer(T* object) noexcept : impl_(new Impl_::impl_base(sc<void*>(object), [](void* p) { std::default_delete<T>{}(sc<T*>(p)); }, false)) {
impl_ = new Impl_::impl<T>(object, false);
increment(); increment();
} }
@ -63,7 +62,7 @@ namespace Hyprutils {
return *this; return *this;
} }
CUniquePointer& operator=(CUniquePointer&& rhs) { CUniquePointer& operator=(CUniquePointer&& rhs) noexcept {
std::swap(impl_, rhs.impl_); std::swap(impl_, rhs.impl_);
return *this; return *this;
} }

View file

@ -16,9 +16,9 @@ namespace Hyprutils {
class CWeakPointer { class CWeakPointer {
public: public:
template <typename X> template <typename X>
using validHierarchy = typename std::enable_if<std::is_assignable<CWeakPointer<T>&, X>::value, CWeakPointer&>::type; using validHierarchy = std::enable_if_t<std::is_assignable_v<CWeakPointer<T>&, X>, CWeakPointer&>;
template <typename X> template <typename X>
using isConstructible = typename std::enable_if<std::is_constructible<T&, X&>::value>::type; using isConstructible = std::enable_if_t<std::is_constructible_v<T&, X&>>;
/* create a weak ptr from a reference */ /* create a weak ptr from a reference */
template <typename U, typename = isConstructible<U>> template <typename U, typename = isConstructible<U>>
@ -26,7 +26,8 @@ namespace Hyprutils {
if (!ref.impl_) if (!ref.impl_)
return; return;
impl_ = ref.impl_; impl_ = ref.impl_;
m_data = ref.m_data;
incrementWeak(); incrementWeak();
} }
@ -36,7 +37,8 @@ namespace Hyprutils {
if (!ref.impl_) if (!ref.impl_)
return; return;
impl_ = ref.impl_; impl_ = ref.impl_;
m_data = ref.impl_->getData();
incrementWeak(); incrementWeak();
} }
@ -46,7 +48,8 @@ namespace Hyprutils {
if (!ref.impl_) if (!ref.impl_)
return; return;
impl_ = ref.impl_; impl_ = ref.impl_;
m_data = ref.m_data;
incrementWeak(); incrementWeak();
} }
@ -54,17 +57,28 @@ namespace Hyprutils {
if (!ref.impl_) if (!ref.impl_)
return; return;
impl_ = ref.impl_; impl_ = ref.impl_;
m_data = ref.m_data;
incrementWeak(); incrementWeak();
} }
CWeakPointer(Impl_::impl_base* implementation, void* data) noexcept : impl_(implementation), m_data(data) {
incrementWeak();
}
CWeakPointer(std::nullptr_t) noexcept {
; // empty
}
template <typename U, typename = isConstructible<U>> template <typename U, typename = isConstructible<U>>
CWeakPointer(CWeakPointer<U>&& ref) noexcept { CWeakPointer(CWeakPointer<U>&& ref) noexcept {
std::swap(impl_, ref.impl_); std::swap(impl_, ref.impl_);
std::swap(m_data, ref.m_data);
} }
CWeakPointer(CWeakPointer&& ref) noexcept { CWeakPointer(CWeakPointer&& ref) noexcept {
std::swap(impl_, ref.impl_); std::swap(impl_, ref.impl_);
std::swap(m_data, ref.m_data);
} }
/* create a weak ptr from another weak ptr with assignment */ /* create a weak ptr from another weak ptr with assignment */
@ -74,7 +88,8 @@ namespace Hyprutils {
return *this; return *this;
decrementWeak(); decrementWeak();
impl_ = rhs.impl_; impl_ = rhs.impl_;
m_data = rhs.m_data;
incrementWeak(); incrementWeak();
return *this; return *this;
} }
@ -84,7 +99,8 @@ namespace Hyprutils {
return *this; return *this;
decrementWeak(); decrementWeak();
impl_ = rhs.impl_; impl_ = rhs.impl_;
m_data = rhs.m_data;
incrementWeak(); incrementWeak();
return *this; return *this;
} }
@ -96,7 +112,8 @@ namespace Hyprutils {
return *this; return *this;
decrementWeak(); decrementWeak();
impl_ = rhs.impl_; impl_ = rhs.impl_;
m_data = rhs.m_data;
incrementWeak(); incrementWeak();
return *this; return *this;
} }
@ -125,14 +142,15 @@ namespace Hyprutils {
void reset() { void reset() {
decrementWeak(); decrementWeak();
impl_ = nullptr; impl_ = nullptr;
m_data = nullptr;
} }
CSharedPointer<T> lock() const { CSharedPointer<T> lock() const {
if (!impl_ || !impl_->dataNonNull() || impl_->destroying() || !impl_->lockable()) if (!impl_ || !impl_->dataNonNull() || impl_->destroying() || !impl_->lockable())
return {}; return {};
return CSharedPointer<T>(impl_); return CSharedPointer<T>(impl_, m_data);
} }
/* this returns valid() */ /* this returns valid() */
@ -169,7 +187,7 @@ namespace Hyprutils {
} }
T* get() const { T* get() const {
return impl_ ? sc<T*>(impl_->getData()) : nullptr; return impl_ && impl_->dataNonNull() ? sc<T*>(m_data) : nullptr;
} }
T* operator->() const { T* operator->() const {
@ -182,6 +200,9 @@ namespace Hyprutils {
Impl_::impl_base* impl_ = nullptr; Impl_::impl_base* impl_ = nullptr;
// Never use directly: raw data ptr, could be UAF
void* m_data = nullptr;
private: private:
/* no-op if there is no impl_ */ /* no-op if there is no impl_ */
void decrementWeak() { void decrementWeak() {
@ -207,6 +228,16 @@ namespace Hyprutils {
impl_->incWeak(); impl_->incWeak();
} }
}; };
template <typename T, typename U>
CWeakPointer<T> dynamicPointerCast(const CWeakPointer<U>& ref) {
if (!ref)
return nullptr;
T* newPtr = dynamic_cast<T*>(sc<U*>(ref.impl_->getData()));
if (!newPtr)
return nullptr;
return CWeakPointer<T>(ref.impl_, newPtr);
}
} }
} }

View file

@ -0,0 +1,9 @@
#pragma once
#include <string>
#include <expected>
#include <string_view>
namespace Hyprutils::File {
std::expected<std::string, std::string> readFileAsString(const std::string_view& path);
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "../string/VarList.hpp" #include "../string/VarList.hpp"
#include <expected>
#include <string> #include <string>
#include <optional> #include <optional>
#include <utility> #include <utility>
@ -12,12 +13,26 @@ namespace Hyprutils {
*/ */
bool checkConfigExists(const std::string basePath, const std::string programName); bool checkConfigExists(const std::string basePath, const std::string programName);
/** Check whether a config in the form basePath/hypr/programName.{extension} exists.
@param basePath the path where the config will be searched
@param programName name of the program (and config file) to search for
@param extension extension of the config file
*/
bool checkConfigExists(const std::string basePath, const std::string programName, const std::string extension);
/** Constructs a full config path given the basePath and programName. /** Constructs a full config path given the basePath and programName.
@param basePath the path where the config hypr/programName.conf is located @param basePath the path where the config hypr/programName.conf is located
@param programName name of the program (and config file) @param programName name of the program (and config file)
*/ */
std::string fullConfigPath(const std::string basePath, const std::string programName); std::string fullConfigPath(const std::string basePath, const std::string programName);
/** Constructs a full config path given the basePath and programName.
@param basePath the path where the config hypr/programName.conf is located
@param programName name of the program (and config file)
@param extension extension of the config file
*/
std::string fullConfigPath(const std::string basePath, const std::string programName, const std::string extension);
/** Retrieves the absolute path of the $HOME env variable. /** Retrieves the absolute path of the $HOME env variable.
*/ */
std::optional<std::string> getHome(); std::optional<std::string> getHome();
@ -38,5 +53,22 @@ namespace Hyprutils {
using T = std::optional<std::string>; using T = std::optional<std::string>;
std::pair<T, T> findConfig(const std::string programName); std::pair<T, T> findConfig(const std::string programName);
/** Searches for a config according to the XDG Base Directory specification.
Returns a pair of the full path to a config and the base path.
Returns std::nullopt in case of a non-existent value.
@param programName name of the program (and config file)
@param extension extension of the config file
*/
std::pair<T, T> findConfig(const std::string programName, const std::string extension);
/** Resolves a path, expanding ~, ., and .. components relative to the given base.
If base is empty, the current working directory is used as the base for relative paths.
Returns the resolved absolute path, or an error string on failure.
@param path the path to resolve (may use ~, ., .., or be absolute)
@param base optional base directory for resolving relative paths
*/
std::expected<std::string, std::string> resolvePath(const std::string& path, const std::string& base = "");
} }
} }

View file

@ -26,10 +26,13 @@ namespace Hyprutils {
template <typename... Args> template <typename... Args>
class CSignalT : public CSignalBase { class CSignalT : public CSignalBase {
template <typename T> template <typename T>
using RefArg = std::conditional_t<std::is_reference_v<T> || std::is_arithmetic_v<T>, T, const T&>; using RefArg = std::conditional_t<std::is_trivially_copyable_v<T>, T, const T&>;
public: public:
void emit(RefArg<Args>... args) { void emit(RefArg<Args>... args) {
if (m_vListeners.empty() && m_vStaticListeners.empty())
return;
if constexpr (sizeof...(Args) == 0) if constexpr (sizeof...(Args) == 0)
emitInternal(nullptr); emitInternal(nullptr);
else { else {

View file

@ -0,0 +1,102 @@
#pragma once
#include <string_view>
#include <cstdint>
#include <expected>
#include <charconv>
#include <concepts>
#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION < 200000
#include <string>
#include <cstdlib>
#include <cerrno>
#endif
namespace Hyprutils::String {
enum eNumericParseResult : uint8_t {
NUMERIC_PARSE_OK = 0,
NUMERIC_PARSE_GARBAGE,
NUMERIC_PARSE_BAD,
NUMERIC_PARSE_OUT_OF_RANGE
};
template <typename T>
requires std::integral<T> || std::floating_point<T>
std::expected<T, eNumericParseResult> strToNumber(std::string_view sv) {
if (sv.empty())
return std::unexpected(NUMERIC_PARSE_BAD);
T value{};
if constexpr (std::integral<T>) {
if (sv.size() >= 2 && sv[0] == '0' && (sv[1] == 'x' || sv[1] == 'X')) {
if (sv.size() == 2)
return std::unexpected(NUMERIC_PARSE_BAD);
const auto hex = sv.substr(2);
const auto [ptr, ec] = std::from_chars(hex.data(), hex.data() + hex.size(), value, 16);
if (ec == std::errc::invalid_argument)
return std::unexpected(NUMERIC_PARSE_BAD);
if (ec == std::errc::result_out_of_range)
return std::unexpected(NUMERIC_PARSE_OUT_OF_RANGE);
if (ptr != hex.data() + hex.size())
return std::unexpected(NUMERIC_PARSE_GARBAGE);
return value;
}
}
#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION < 200000
// libc++ < 20 does not implement std::from_chars for floating point types
if constexpr (std::floating_point<T>) {
std::string_view ts = sv;
if (ts.starts_with('+') || ts.starts_with('-'))
ts.remove_prefix(1);
if (ts.size() >= 2 && ts[0] == '0' && (ts[1] == 'x' || ts[1] == 'X'))
return std::unexpected(NUMERIC_PARSE_GARBAGE);
std::string s{sv};
char* endptr = nullptr;
errno = 0;
if constexpr (std::same_as<T, float>)
value = std::strtof(s.c_str(), &endptr);
else if constexpr (std::same_as<T, double>)
value = std::strtod(s.c_str(), &endptr);
else
value = std::strtold(s.c_str(), &endptr);
if (endptr == s.c_str())
return std::unexpected(NUMERIC_PARSE_BAD);
if (errno == ERANGE)
return std::unexpected(NUMERIC_PARSE_OUT_OF_RANGE);
if (endptr != s.c_str() + s.size())
return std::unexpected(NUMERIC_PARSE_GARBAGE);
return value;
} else {
const auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value);
if (ec == std::errc::invalid_argument)
return std::unexpected(NUMERIC_PARSE_BAD);
if (ec == std::errc::result_out_of_range)
return std::unexpected(NUMERIC_PARSE_OUT_OF_RANGE);
if (ptr != sv.data() + sv.size())
return std::unexpected(NUMERIC_PARSE_GARBAGE);
return value;
}
#else
const auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value);
if (ec == std::errc::invalid_argument)
return std::unexpected(NUMERIC_PARSE_BAD);
if (ec == std::errc::result_out_of_range)
return std::unexpected(NUMERIC_PARSE_OUT_OF_RANGE);
if (ptr != sv.data() + sv.size())
return std::unexpected(NUMERIC_PARSE_GARBAGE);
return value;
#endif
}
};

View file

@ -4,8 +4,12 @@
namespace Hyprutils { namespace Hyprutils {
namespace String { namespace String {
// trims beginning and end of whitespace characters // trims beginning and end of whitespace characters
std::string trim(const std::string& in); std::string trim(const char* in);
bool isNumber(const std::string& str, bool allowfloat = false); std::string trim(const std::string& in);
void replaceInString(std::string& string, const std::string& what, const std::string& to); std::string_view trim(const std::string_view& in);
bool isNumber(const std::string& str, bool allowfloat = false);
bool isNumber2(const std::string_view& str, bool allowfloat = false);
void replaceInString(std::string& string, const std::string& what, const std::string& to);
bool truthy(const std::string_view& in);
}; };
}; };

View file

@ -0,0 +1,71 @@
#pragma once
#include <functional>
#include <vector>
#include <string>
#include <string_view>
namespace Hyprutils {
namespace String {
class CVarList2 {
public:
/** Split string into arg list
Prefer this over CConstVarList / CVarList, this is better.
@param lastArgNo stop splitting after argv reaches maximum size, last arg will contain rest of unsplit args
@param delim if delimiter is 's', use std::isspace
@param removeEmpty remove empty args from argv
@param allowEscape whether to allow escaping the delimiter
*/
explicit CVarList2(std::string&& in, const size_t lastArgNo = 0, const char delim = ',', const bool removeEmpty = false, const bool allowEscape = true);
/** Split string into arg list
Prefer this over CConstVarList / CVarList, this is better.
Warning: passing a string_view will make this list assume sv is kept alive as long as this vl is alive.
@param lastArgNo stop splitting after argv reaches maximum size, last arg will contain rest of unsplit args
@param delim if delimiter is 's', use std::isspace
@param removeEmpty remove empty args from argv
@param allowEscape whether to allow escaping the delimiter
*/
explicit CVarList2(std::string_view in, const size_t lastArgNo = 0, const char delim = ',', const bool removeEmpty = false, const bool allowEscape = true);
/**
Same as CVarList2(std::string_view, ...)
*/
explicit CVarList2(const char* in, const size_t lastArgNo = 0, const char delim = ',', const bool removeEmpty = false, const bool allowEscape = true);
~CVarList2() = default;
size_t size() const {
return m_args.size();
}
std::string join(const std::string& joiner, size_t from = 0, size_t to = 0) const;
void append(std::string&& arg);
bool contains(const std::string& el);
std::string_view operator[](const size_t& idx) const {
if (idx >= m_args.size())
return "";
return m_args[idx];
}
// for range-based loops
std::vector<std::string_view>::const_iterator begin() const {
return m_args.begin();
}
std::vector<std::string_view>::const_iterator end() const {
return m_args.end();
}
private:
void construct(std::string_view in, const size_t lastArgNo, const char delim, const bool removeEmpty, const bool allowEscape);
std::string m_inStringCopy;
std::string_view m_inString;
std::vector<std::string> m_copyStrings;
std::vector<std::string_view> m_args;
};
}
}

View file

@ -4,16 +4,17 @@
stdenvAdapters, stdenvAdapters,
cmake, cmake,
pkg-config, pkg-config,
gtest,
pixman, pixman,
version ? "git", version ? "git",
doCheck ? false,
debug ? false, debug ? false,
# whether to use the mold linker # whether to use the mold linker
# disable this for older machines without SSE4_2 and AVX2 support # disable this for older machines without SSE4_2 and AVX2 support
withMold ? true, withMold ? true,
}: let }:
let
inherit (builtins) foldl'; inherit (builtins) foldl';
inherit (lib.lists) flatten; inherit (lib.lists) flatten optional;
inherit (lib.strings) optionalString; inherit (lib.strings) optionalString;
adapters = flatten [ adapters = flatten [
@ -23,31 +24,34 @@
customStdenv = foldl' (acc: adapter: adapter acc) stdenv adapters; customStdenv = foldl' (acc: adapter: adapter acc) stdenv adapters;
in in
customStdenv.mkDerivation { customStdenv.mkDerivation {
pname = "hyprutils" + optionalString debug "-debug"; pname = "hyprutils" + optionalString debug "-debug";
inherit version doCheck; inherit version;
src = ../.; src = ../.;
nativeBuildInputs = [ doCheck = debug;
cmake
pkg-config
];
buildInputs = [ nativeBuildInputs = [
pixman cmake
]; pkg-config
];
outputs = ["out" "dev"]; buildInputs = flatten [
(optional debug gtest)
pixman
];
cmakeBuildType = outputs = [
if debug "out"
then "Debug" "dev"
else "RelWithDebInfo"; ];
meta = with lib; { cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
homepage = "https://github.com/hyprwm/hyprutils";
description = "Small C++ library for utilities used across the Hypr* ecosystem"; meta = with lib; {
license = licenses.bsd3; homepage = "https://github.com/hyprwm/hyprutils";
platforms = platforms.linux; description = "Small C++ library for utilities used across the Hypr* ecosystem";
}; license = licenses.bsd3;
} platforms = platforms.linux;
};
}

View file

@ -1,23 +1,28 @@
{ {
self, self,
lib, lib,
}: let }:
mkDate = longDate: (lib.concatStringsSep "-" [ let
(builtins.substring 0 4 longDate) mkDate =
(builtins.substring 4 2 longDate) longDate:
(builtins.substring 6 2 longDate) (lib.concatStringsSep "-" [
]); (builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION); ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION);
version = ver + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); version =
in { ver + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
in
{
default = self.overlays.hyprutils; default = self.overlays.hyprutils;
hyprutils = final: prev: { hyprutils = final: prev: {
hyprutils = final.callPackage ./default.nix { hyprutils = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv; stdenv = final.gcc15Stdenv;
inherit version; inherit version;
}; };
hyprutils-debug = final.hyprutils.override {debug = true;}; hyprutils-debug = final.hyprutils.override { debug = true; };
hyprutils-with-tests = final.hyprutils.override {doCheck = true;}; hyprutils-with-tests = final.hyprutils-debug;
}; };
} }

View file

@ -2,18 +2,31 @@
#include <hyprutils/animation/AnimationManager.hpp> #include <hyprutils/animation/AnimationManager.hpp>
#include <hyprutils/memory/WeakPtr.hpp> #include <hyprutils/memory/WeakPtr.hpp>
#include <algorithm>
#include <cmath>
using namespace Hyprutils::Animation; using namespace Hyprutils::Animation;
using namespace Hyprutils::Memory; using namespace Hyprutils::Memory;
static const std::string DEFAULTBEZIERNAME = "default"; static const std::string DEFAULTBEZIERNAME = "default";
static const std::string DEFAULTSTYLE = ""; static const std::string DEFAULTSTYLE = "";
static constexpr std::string_view SPRINGPREFIX = "spring:";
#define SP CSharedPointer #define SP CSharedPointer
#define WP CWeakPointer #define WP CWeakPointer
void CBaseAnimatedVariable::create(CAnimationManager* pManager, int typeInfo, SP<CBaseAnimatedVariable> pSelf) { void CBaseAnimatedVariable::create(CAnimationManager* pManager, int typeInfo, SP<CBaseAnimatedVariable> pSelf) {
m_Type = typeInfo; m_Type = typeInfo;
m_pSelf = pSelf; m_pSelf = std::move(pSelf);
m_pAnimationManager = pManager;
m_pSignals = pManager->getSignals();
m_bDummy = false;
}
void CBaseAnimatedVariable::create2(CAnimationManager* pManager, int typeInfo, WP<CBaseAnimatedVariable> pSelf) {
m_Type = typeInfo;
m_pSelf = std::move(pSelf);
m_pAnimationManager = pManager; m_pAnimationManager = pManager;
m_pSignals = pManager->getSignals(); m_pSignals = pManager->getSignals();
@ -37,28 +50,22 @@ void CBaseAnimatedVariable::disconnectFromActive() {
} }
bool Hyprutils::Animation::CBaseAnimatedVariable::enabled() const { bool Hyprutils::Animation::CBaseAnimatedVariable::enabled() const {
if (const auto PCONFIG = m_pConfig.lock()) { if (m_pConfig && m_pConfig->pValues)
const auto PVALUES = PCONFIG->pValues.lock(); return m_pConfig->pValues->internalEnabled;
return PVALUES ? PVALUES->internalEnabled : false;
}
return false; return false;
} }
const std::string& CBaseAnimatedVariable::getBezierName() const { const std::string& CBaseAnimatedVariable::getBezierName() const {
if (const auto PCONFIG = m_pConfig.lock()) { if (m_pConfig && m_pConfig->pValues)
const auto PVALUES = PCONFIG->pValues.lock(); return m_pConfig->pValues->internalBezier;
return PVALUES ? PVALUES->internalBezier : DEFAULTBEZIERNAME;
}
return DEFAULTBEZIERNAME; return DEFAULTBEZIERNAME;
} }
const std::string& CBaseAnimatedVariable::getStyle() const { const std::string& CBaseAnimatedVariable::getStyle() const {
if (const auto PCONFIG = m_pConfig.lock()) { if (m_pConfig && m_pConfig->pValues)
const auto PVALUES = PCONFIG->pValues.lock(); return m_pConfig->pValues->internalStyle;
return PVALUES ? PVALUES->internalStyle : DEFAULTSTYLE;
}
return DEFAULTSTYLE; return DEFAULTSTYLE;
} }
@ -66,36 +73,92 @@ const std::string& CBaseAnimatedVariable::getStyle() const {
float CBaseAnimatedVariable::getPercent() const { float CBaseAnimatedVariable::getPercent() const {
const auto DURATIONPASSED = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - animationBegin).count(); const auto DURATIONPASSED = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - animationBegin).count();
if (const auto PCONFIG = m_pConfig.lock()) { if (m_pConfig && m_pConfig->pValues)
const auto PVALUES = PCONFIG->pValues.lock(); return std::clamp((DURATIONPASSED / 100.F) / m_pConfig->pValues->internalSpeed, 0.f, 1.f);
return PVALUES ? std::clamp((DURATIONPASSED / 100.f) / PVALUES->internalSpeed, 0.f, 1.f) : 1.f;
}
return 1.f; return 1.F;
} }
float CBaseAnimatedVariable::getCurveValue() const { float CBaseAnimatedVariable::getCurveValue() const {
if (!m_bIsBeingAnimated || isAnimationManagerDead()) if (!m_bIsBeingAnimated || isAnimationManagerDead())
return 1.f; return 1.F;
std::string bezierName = ""; if (isSpringCurve())
if (const auto PCONFIG = m_pConfig.lock()) { return m_fSpringValue;
const auto PVALUES = PCONFIG->pValues.lock();
if (PVALUES)
bezierName = PVALUES->internalBezier;
}
const auto BEZIER = m_pAnimationManager->getBezier(bezierName); const auto BEZIER = m_pAnimationManager->getBezier(getBezierName());
if (!BEZIER) if (!BEZIER)
return 1.f; return 1.F;
const auto SPENT = getPercent(); const auto SPENT = getPercent();
if (SPENT >= 1.f) if (SPENT >= 1.F)
return 1.f; return 1.F;
return BEZIER->getYForPoint(SPENT); return BEZIER->getYForPoint(SPENT);
} }
CBaseAnimatedVariable::SCurveStepResult CBaseAnimatedVariable::getCurveStep() {
if (!m_bIsBeingAnimated || isAnimationManagerDead())
return {};
if (!isSpringCurve()) {
const auto SPENT = getPercent();
if (SPENT >= 1.f)
return {.value = 1.F, .finished = true};
const auto BEZIER = m_pAnimationManager->getBezier(getBezierName());
if (!BEZIER)
return {.value = 1.F, .finished = true};
return {
.value = BEZIER->getYForPoint(SPENT),
.finished = false,
};
}
const auto SPRINGNAME = springNameFromSpec(getBezierName());
const auto SPRING = m_pAnimationManager->getSpring(std::string{SPRINGNAME});
if (!SPRING)
return {.value = 1.F, .finished = true};
const auto NOW = std::chrono::steady_clock::now();
float dt = std::chrono::duration<float>(NOW - springLastStep).count();
springLastStep = NOW;
constexpr float MINDELTA = 1.F / 240.F;
if (dt <= 0.F)
dt = MINDELTA;
else
dt = std::clamp(dt, MINDELTA, 0.05F);
if (dt > 0.F) {
constexpr const float FIXEDSTEP = 1.F / 240.F;
const int SUBSTEPS = std::clamp(static_cast<int>(std::ceil(dt / FIXEDSTEP)), 1, 16);
const float STEPTIME = dt / SUBSTEPS;
const float MASS = std::max(SPRING->mass, 0.0001f);
for (int i = 0; i < SUBSTEPS; ++i) {
const float displacement = m_fSpringValue - 1.f;
const float acceleration = ((-SPRING->stiffness * displacement) - (SPRING->damping * m_fSpringVelocity)) / MASS;
m_fSpringVelocity += acceleration * STEPTIME;
m_fSpringValue += m_fSpringVelocity * STEPTIME;
}
}
const bool FINISHED = std::abs(1.F - m_fSpringValue) <= SPRING->valueEpsilon && std::abs(m_fSpringVelocity) <= SPRING->velocityEpsilon;
if (FINISHED) {
m_fSpringValue = 1.F;
m_fSpringVelocity = 0.F;
}
return {.value = m_fSpringValue, .finished = FINISHED};
}
bool CBaseAnimatedVariable::isSpringCurve() const {
return !springNameFromSpec(getBezierName()).empty();
}
bool CBaseAnimatedVariable::ok() const { bool CBaseAnimatedVariable::ok() const {
return m_pConfig && !m_bDummy && !isAnimationManagerDead(); return m_pConfig && !m_bDummy && !isAnimationManagerDead();
} }
@ -144,7 +207,10 @@ void CBaseAnimatedVariable::onAnimationEnd() {
} }
} }
void CBaseAnimatedVariable::onAnimationBegin() { void CBaseAnimatedVariable::onAnimationBegin(bool preserveCurveState, float springVelocityScale) {
if (isSpringCurve())
resetSpringState(preserveCurveState, springVelocityScale);
m_bIsBeingAnimated = true; m_bIsBeingAnimated = true;
animationBegin = std::chrono::steady_clock::now(); animationBegin = std::chrono::steady_clock::now();
connectToActive(); connectToActive();
@ -159,3 +225,20 @@ void CBaseAnimatedVariable::onAnimationBegin() {
bool CBaseAnimatedVariable::isAnimationManagerDead() const { bool CBaseAnimatedVariable::isAnimationManagerDead() const {
return m_pSignals.expired(); return m_pSignals.expired();
} }
void CBaseAnimatedVariable::resetSpringState(bool preserveVelocity, float velocityScale) {
m_fSpringValue = 0.f;
if (!preserveVelocity)
m_fSpringVelocity = 0.f;
else
m_fSpringVelocity *= velocityScale;
springLastStep = std::chrono::steady_clock::now();
}
std::string_view CBaseAnimatedVariable::springNameFromSpec(const std::string& spec) const {
if (!spec.starts_with(SPRINGPREFIX) || spec.size() <= SPRINGPREFIX.size())
return {};
return std::string_view(spec).substr(SPRINGPREFIX.size());
}

View file

@ -11,11 +11,14 @@ using namespace Hyprutils::Signal;
#define WP CWeakPointer #define WP CWeakPointer
const std::array<Vector2D, 2> DEFAULTBEZIERPOINTS = {Vector2D(0.0, 0.75), Vector2D(0.15, 1.0)}; const std::array<Vector2D, 2> DEFAULTBEZIERPOINTS = {Vector2D(0.0, 0.75), Vector2D(0.15, 1.0)};
const SSpringCurve DEFAULTSPRING = {};
CAnimationManager::CAnimationManager() { CAnimationManager::CAnimationManager() {
const auto BEZIER = makeShared<CBezierCurve>(); const auto BEZIER = makeShared<CBezierCurve>();
BEZIER->setup(DEFAULTBEZIERPOINTS); BEZIER->setup(DEFAULTBEZIERPOINTS);
m_mBezierCurves["default"] = BEZIER; m_mBezierCurves["default"] = BEZIER;
m_mSpringCurves["default"] = makeShared<SSpringCurve>(DEFAULTSPRING);
m_events = makeUnique<SAnimationManagerSignals>(); m_events = makeUnique<SAnimationManagerSignals>();
m_listeners = makeUnique<SAnimVarListeners>(); m_listeners = makeUnique<SAnimVarListeners>();
@ -43,15 +46,21 @@ void CAnimationManager::removeAllBeziers() {
m_mBezierCurves["default"] = BEZIER; m_mBezierCurves["default"] = BEZIER;
} }
void CAnimationManager::removeAllSprings() {
m_mSpringCurves.clear();
m_mSpringCurves["default"] = makeShared<SSpringCurve>(DEFAULTSPRING);
}
void CAnimationManager::addBezierWithName(std::string name, const Vector2D& p1, const Vector2D& p2) { void CAnimationManager::addBezierWithName(std::string name, const Vector2D& p1, const Vector2D& p2) {
const auto BEZIER = makeShared<CBezierCurve>(); const auto BEZIER = makeShared<CBezierCurve>();
BEZIER->setup({ BEZIER->setup({p1, p2});
p1,
p2,
});
m_mBezierCurves[name] = BEZIER; m_mBezierCurves[name] = BEZIER;
} }
void CAnimationManager::addSpringWithName(std::string name, const SSpringCurve& spring) {
m_mSpringCurves[name] = makeShared<SSpringCurve>(spring);
}
bool CAnimationManager::shouldTickForNext() { bool CAnimationManager::shouldTickForNext() {
return !m_vActiveAnimatedVariables.empty(); return !m_vActiveAnimatedVariables.empty();
} }
@ -64,14 +73,13 @@ void CAnimationManager::rotateActive() {
std::vector<CWeakPointer<CBaseAnimatedVariable>> active; std::vector<CWeakPointer<CBaseAnimatedVariable>> active;
active.reserve(m_vActiveAnimatedVariables.size()); // avoid reallocations active.reserve(m_vActiveAnimatedVariables.size()); // avoid reallocations
for (auto const& av : m_vActiveAnimatedVariables) { for (auto const& av : m_vActiveAnimatedVariables) {
const auto PAV = av.lock(); if (!av)
if (!PAV)
continue; continue;
if (PAV->ok() && PAV->isBeingAnimated()) if (av->ok() && av->isBeingAnimated())
active.emplace_back(av); active.emplace_back(av);
else else
PAV->m_bIsConnectedToActive = false; av->m_bIsConnectedToActive = false;
} }
m_vActiveAnimatedVariables = std::move(active); m_vActiveAnimatedVariables = std::move(active);
@ -86,16 +94,35 @@ bool CAnimationManager::bezierExists(const std::string& bezier) {
return false; return false;
} }
bool CAnimationManager::springExists(const std::string& spring) {
for (auto const& [sc, cfg] : m_mSpringCurves) {
if (sc == spring)
return true;
}
return false;
}
SP<CBezierCurve> CAnimationManager::getBezier(const std::string& name) { SP<CBezierCurve> CAnimationManager::getBezier(const std::string& name) {
const auto BEZIER = std::ranges::find_if(m_mBezierCurves, [&](const auto& other) { return other.first == name; }); const auto BEZIER = std::ranges::find_if(m_mBezierCurves, [&](const auto& other) { return other.first == name; });
return BEZIER == m_mBezierCurves.end() ? m_mBezierCurves["default"] : BEZIER->second; return BEZIER == m_mBezierCurves.end() ? m_mBezierCurves["default"] : BEZIER->second;
} }
SP<SSpringCurve> CAnimationManager::getSpring(const std::string& name) {
const auto SPRING = std::ranges::find_if(m_mSpringCurves, [&](const auto& other) { return other.first == name; });
return SPRING == m_mSpringCurves.end() ? m_mSpringCurves["default"] : SPRING->second;
}
const std::unordered_map<std::string, SP<CBezierCurve>>& CAnimationManager::getAllBeziers() { const std::unordered_map<std::string, SP<CBezierCurve>>& CAnimationManager::getAllBeziers() {
return m_mBezierCurves; return m_mBezierCurves;
} }
const std::unordered_map<std::string, SP<SSpringCurve>>& CAnimationManager::getAllSprings() {
return m_mSpringCurves;
}
CWeakPointer<CAnimationManager::SAnimationManagerSignals> CAnimationManager::getSignals() const { CWeakPointer<CAnimationManager::SAnimationManagerSignals> CAnimationManager::getSignals() const {
return m_events; return m_events;
} }

View file

@ -9,42 +9,46 @@ using namespace Hyprutils::Math;
using namespace Hyprutils::Memory; using namespace Hyprutils::Memory;
void CBezierCurve::setup(const std::array<Vector2D, 2>& pVec) { void CBezierCurve::setup(const std::array<Vector2D, 2>& pVec) {
// Avoid reallocations by reserving enough memory upfront setup4(std::array<Vector2D, 4>{
m_vPoints.resize(pVec.size() + 2);
m_vPoints = {
Vector2D(0, 0), // Start point Vector2D(0, 0), // Start point
pVec[0], pVec[1], // Control points pVec[0], pVec[1], // Control points
Vector2D(1, 1) // End point Vector2D(1, 1) // End point
});
}
void CBezierCurve::setup4(const std::array<Vector2D, 4>& pVec) {
// Avoid reallocations by reserving enough memory upfront
m_vPoints.resize(4);
m_vPoints = {
pVec[0],
pVec[1],
pVec[2],
pVec[3],
}; };
if (m_vPoints.size() != 4) // Pre-bake curve
std::abort(); //
// We start baking at t=(i+1)/n not at t=0
// bake BAKEDPOINTS points for faster lookups // That means the first baked x can be > 0 if curve itself starts at x>0
// T -> X ( / BAKEDPOINTS )
for (int i = 0; i < BAKEDPOINTS; ++i) { for (int i = 0; i < BAKEDPOINTS; ++i) {
float const t = (i + 1) / sc<float>(BAKEDPOINTS); // When i=0 -> t=1/255
const float t = (i + 1) * INVBAKEDPOINTS;
m_aPointsBaked[i] = Vector2D(getXForT(t), getYForT(t)); m_aPointsBaked[i] = Vector2D(getXForT(t), getYForT(t));
} }
for (int j = 1; j < 10; ++j) {
float i = j / 10.0f;
getYForPoint(i);
}
} }
float CBezierCurve::getXForT(float const& t) const { float CBezierCurve::getXForT(float const& t) const {
float t2 = t * t; float t2 = t * t;
float t3 = t2 * t; float t3 = t2 * t;
return (3 * t * (1 - t) * (1 - t) * m_vPoints[1].x) + (3 * t2 * (1 - t) * m_vPoints[2].x) + (t3 * m_vPoints[3].x); return ((1 - t) * (1 - t) * (1 - t) * m_vPoints[0].x) + (3 * t * (1 - t) * (1 - t) * m_vPoints[1].x) + (3 * t2 * (1 - t) * m_vPoints[2].x) + (t3 * m_vPoints[3].x);
} }
float CBezierCurve::getYForT(float const& t) const { float CBezierCurve::getYForT(float const& t) const {
float t2 = t * t; float t2 = t * t;
float t3 = t2 * t; float t3 = t2 * t;
return (3 * t * (1 - t) * (1 - t) * m_vPoints[1].y) + (3 * t2 * (1 - t) * m_vPoints[2].y) + (t3 * m_vPoints[3].y); return ((1 - t) * (1 - t) * (1 - t) * m_vPoints[0].y) + (3 * t * (1 - t) * (1 - t) * m_vPoints[1].y) + (3 * t2 * (1 - t) * m_vPoints[2].y) + (t3 * m_vPoints[3].y);
} }
// Todo: this probably can be done better and faster // Todo: this probably can be done better and faster
@ -62,21 +66,40 @@ float CBezierCurve::getYForPoint(float const& x) const {
else else
index -= step; index -= step;
// Clamp to avoid index walking off
if (index < 0)
index = 0;
else if (index > BAKEDPOINTS - 1)
index = BAKEDPOINTS - 1;
below = m_aPointsBaked[index].x < x; below = m_aPointsBaked[index].x < x;
} }
int lowerIndex = index - (!below || index == BAKEDPOINTS - 1); int lowerIndex = index - (!below || index == BAKEDPOINTS - 1);
// in the name of performance i shall make a hack // Clamp final indices
const auto LOWERPOINT = &m_aPointsBaked[lowerIndex]; if (lowerIndex < 0)
const auto UPPERPOINT = &m_aPointsBaked[lowerIndex + 1]; lowerIndex = 0;
else if (lowerIndex > BAKEDPOINTS - 2)
lowerIndex = BAKEDPOINTS - 2;
const auto PERCINDELTA = (x - LOWERPOINT->x) / (UPPERPOINT->x - LOWERPOINT->x); // In the name of performance I shall make a hack
const auto& LOWERPOINT = m_aPointsBaked[lowerIndex];
const auto& UPPERPOINT = m_aPointsBaked[lowerIndex + 1];
if (std::isnan(PERCINDELTA) || std::isinf(PERCINDELTA)) // can sometimes happen for VERY small x const float dx = (UPPERPOINT.x - LOWERPOINT.x);
return 0.f; // If two baked points have almost the same x
// just return the lower one
if (dx <= 1e-6f)
return LOWERPOINT.y;
return LOWERPOINT->y + ((UPPERPOINT->y - LOWERPOINT->y) * PERCINDELTA); const auto PERCINDELTA = (x - LOWERPOINT.x) / dx;
// Can sometimes happen for VERY small x
if (std::isnan(PERCINDELTA) || std::isinf(PERCINDELTA))
return LOWERPOINT.y;
return LOWERPOINT.y + ((UPPERPOINT.y - LOWERPOINT.y) * PERCINDELTA);
} }
const std::vector<Hyprutils::Math::Vector2D>& CBezierCurve::getControlPoints() const { const std::vector<Hyprutils::Math::Vector2D>& CBezierCurve::getControlPoints() const {

298
src/cli/ArgumentParser.cpp Normal file
View file

@ -0,0 +1,298 @@
#include "ArgumentParser.hpp"
#include <format>
#include <vector>
#include <algorithm>
#include <hyprutils/string/String.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::CLI;
using namespace Hyprutils::Memory;
using namespace Hyprutils::String;
using namespace Hyprutils;
CArgumentParser::CArgumentParser(const std::span<const char*>& args) : m_impl(makeUnique<CArgumentParserImpl>(args)) {
;
}
std::expected<void, std::string> CArgumentParser::registerBoolOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description) {
return m_impl->registerOption(name, abbrev, description, ARG_TYPE_BOOL);
}
std::expected<void, std::string> CArgumentParser::registerIntOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description) {
return m_impl->registerOption(name, abbrev, description, ARG_TYPE_INT);
}
std::expected<void, std::string> CArgumentParser::registerFloatOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description) {
return m_impl->registerOption(name, abbrev, description, ARG_TYPE_FLOAT);
}
std::expected<void, std::string> CArgumentParser::registerStringOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description) {
return m_impl->registerOption(name, abbrev, description, ARG_TYPE_STR);
}
std::optional<bool> CArgumentParser::getBool(const std::string_view& name) {
auto ref = m_impl->getValue(name);
if (ref == m_impl->m_values.end())
return std::nullopt;
if (const auto pval = std::get_if<bool>(&ref->val); pval)
return *pval;
return std::nullopt;
}
std::optional<int> CArgumentParser::getInt(const std::string_view& name) {
auto ref = m_impl->getValue(name);
if (ref == m_impl->m_values.end())
return std::nullopt;
if (const auto pval = std::get_if<int>(&ref->val); pval)
return *pval;
return std::nullopt;
}
std::optional<float> CArgumentParser::getFloat(const std::string_view& name) {
auto ref = m_impl->getValue(name);
if (ref == m_impl->m_values.end())
return std::nullopt;
if (const auto pval = std::get_if<float>(&ref->val); pval)
return *pval;
return std::nullopt;
}
std::optional<std::string_view> CArgumentParser::getString(const std::string_view& name) {
auto ref = m_impl->getValue(name);
if (ref == m_impl->m_values.end())
return std::nullopt;
if (const auto pval = std::get_if<std::string>(&ref->val); pval)
return *pval;
return std::nullopt;
}
std::string CArgumentParser::getDescription(const std::string_view& header, std::optional<size_t> maxWidth) {
return m_impl->getDescription(header, maxWidth);
}
std::expected<void, std::string> CArgumentParser::parse() {
return m_impl->parse();
}
CArgumentParserImpl::CArgumentParserImpl(const std::span<const char*>& args) {
m_argv.reserve(args.size());
for (const auto& a : args) {
m_argv.emplace_back(a);
}
}
std::vector<SArgumentKey>::iterator CArgumentParserImpl::getValue(const std::string_view& sv) {
if (sv.empty())
return m_values.end();
auto it = std::ranges::find_if(m_values, [&sv](const auto& e) { return e.full == sv || e.abbrev == sv; });
return it;
}
std::expected<void, std::string> CArgumentParserImpl::registerOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description,
eArgumentType type) {
if (name.empty())
return std::unexpected("Name cannot be empty");
if (getValue(name) != m_values.end() || getValue(abbrev) != m_values.end())
return std::unexpected("Value already exists");
m_values.emplace_back(SArgumentKey{
.full = std::string{name},
.abbrev = std::string{abbrev},
.desc = std::string{description},
.argType = type,
.val = std::monostate{},
});
return {};
}
std::expected<void, std::string> CArgumentParserImpl::parse() {
// walk the args
for (size_t i = 1; i < m_argv.size(); ++i) {
auto val = m_values.end();
const auto& arg = m_argv.at(i);
if (arg.starts_with("--"))
val = getValue(std::string_view{arg}.substr(2));
else if (arg.starts_with('-'))
val = getValue(std::string_view{arg}.substr(1));
else
return std::unexpected(std::format("Invalid element found while parsing: {}", arg));
if (val == m_values.end())
return std::unexpected(std::format("Invalid argument found while parsing: {}", arg));
switch (val->argType) {
case ARG_TYPE_BOOL: {
val->val = true;
break;
}
case ARG_TYPE_INT: {
if (i + 1 >= m_argv.size())
return std::unexpected(std::format("Failed parsing argument {}, no value supplied", arg));
const auto& next = std::string{m_argv.at(++i)};
if (!isNumber(next))
return std::unexpected(std::format("Failed parsing argument {}, value {} is not an int", arg, next));
try {
val->val = sc<int>(std::stoi(next));
} catch (...) { return std::unexpected(std::format("Failed parsing argument {}, value {} is not an int", arg, next)); }
break;
}
case ARG_TYPE_FLOAT: {
if (i + 1 >= m_argv.size())
return std::unexpected(std::format("Failed parsing argument {}, no value supplied", arg));
const auto& next = std::string{m_argv.at(++i)};
if (!isNumber(next, true))
return std::unexpected(std::format("Failed parsing argument {}, value {} is not a float", arg, next));
try {
val->val = sc<float>(std::stof(next));
} catch (...) { return std::unexpected(std::format("Failed parsing argument {}, value {} is not a float", arg, next)); }
break;
}
case ARG_TYPE_STR: {
if (i + 1 >= m_argv.size())
return std::unexpected(std::format("Failed parsing argument {}, no value supplied", arg));
val->val = std::string{m_argv.at(++i)};
break;
}
case ARG_TYPE_END: break;
}
}
return {};
}
std::string CArgumentParserImpl::getDescription(const std::string_view& header, std::optional<size_t> maxWidth) {
const size_t MAX_COLS = maxWidth.value_or(80);
const std::string PAD_STR = " ";
constexpr const std::array<const char*, ARG_TYPE_END> TYPE_STRS = {
"", // bool
"[int]", // int
"[float]", // float
"[str]", // str
};
//
auto wrap = [](const std::string_view& str, size_t maxW) -> std::vector<std::string_view> {
std::vector<std::string_view> result;
// walk word by word
size_t nextSpacePos = 0, lastBreakPos = 0;
while (true) {
size_t lastSpacePos = nextSpacePos;
nextSpacePos = str.find(' ', nextSpacePos + 1);
if (nextSpacePos == std::string::npos)
break;
if (nextSpacePos - lastBreakPos > maxW) {
if (lastSpacePos - lastBreakPos <= maxW) {
// break
result.emplace_back(str.substr(lastBreakPos, lastSpacePos - lastBreakPos));
lastBreakPos = lastSpacePos + 1;
} else {
while (lastSpacePos - lastBreakPos > maxW) {
// break
result.emplace_back(str.substr(lastBreakPos, maxW));
lastBreakPos += maxW;
}
}
continue;
}
}
result.emplace_back(str.substr(lastBreakPos));
return result;
};
auto pad = [&PAD_STR](size_t len) -> std::string_view {
if (len >= PAD_STR.size())
return PAD_STR;
return std::string_view{PAD_STR}.substr(0, len);
};
std::string rolling;
rolling += std::format("┏ {}\n", header);
rolling += "";
for (size_t i = 0; i < MAX_COLS; ++i) {
rolling += "";
}
rolling += "\n";
// get max widths
size_t maxArgWidth = 0, maxShortWidth = 0;
for (const auto& v : m_values) {
maxShortWidth = std::max(maxShortWidth, v.abbrev.size() + 4 + std::string_view{TYPE_STRS[v.argType]}.length());
maxArgWidth = std::max(maxArgWidth, v.full.size() + 3);
}
// write the table
for (const auto& v : m_values) {
size_t lenUsed = 0;
rolling += "┣ --" + v.full;
lenUsed += 3 + v.full.size();
rolling += pad(maxArgWidth - lenUsed);
lenUsed = maxArgWidth;
if (!v.abbrev.empty()) {
rolling += " -" + v.abbrev;
lenUsed += 2 + v.abbrev.size();
rolling += " ";
rolling += TYPE_STRS[v.argType];
lenUsed += std::string_view{TYPE_STRS[v.argType]}.length() + 1;
rolling += pad(maxArgWidth + maxShortWidth - lenUsed);
} else
rolling += pad(maxShortWidth);
lenUsed = maxArgWidth + maxShortWidth;
rolling += " | ";
lenUsed += 3;
const auto ROWS = wrap(v.desc, MAX_COLS - lenUsed);
const auto LEN_START_DESC = lenUsed;
rolling += ROWS[0];
lenUsed += ROWS[0].size();
rolling += pad(MAX_COLS - lenUsed);
rolling += "\n";
for (size_t i = 1; i < ROWS.size(); ++i) {
lenUsed = LEN_START_DESC;
rolling += "";
rolling += pad(LEN_START_DESC);
rolling += ROWS[i];
lenUsed += ROWS[i].size();
rolling += pad(MAX_COLS - lenUsed);
rolling += "\n";
}
}
rolling += "";
for (size_t i = 0; i < MAX_COLS; ++i) {
rolling += "";
}
rolling += "\n";
return rolling;
}

View file

@ -0,0 +1,39 @@
#include <hyprutils/cli/ArgumentParser.hpp>
#include <map>
#include <variant>
#include <vector>
namespace Hyprutils::CLI {
enum eArgumentType : uint8_t {
ARG_TYPE_BOOL = 0,
ARG_TYPE_INT,
ARG_TYPE_FLOAT,
ARG_TYPE_STR,
ARG_TYPE_END,
};
struct SArgumentKey {
using Value = std::variant<std::monostate, bool, int, float, std::string>;
std::string full, abbrev, desc;
eArgumentType argType = ARG_TYPE_BOOL;
Value val;
};
class CArgumentParserImpl {
public:
CArgumentParserImpl(const std::span<const char*>& args);
~CArgumentParserImpl() = default;
std::string getDescription(const std::string_view& header, std::optional<size_t> maxWidth = {});
std::expected<void, std::string> parse();
std::vector<SArgumentKey>::iterator getValue(const std::string_view& sv);
std::expected<void, std::string> registerOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description, eArgumentType type);
std::vector<SArgumentKey> m_values;
std::vector<std::string_view> m_argv;
};
}

202
src/cli/Logger.cpp Normal file
View file

@ -0,0 +1,202 @@
#include "Logger.hpp"
#include <chrono>
#include <fcntl.h>
#include <print>
using namespace Hyprutils;
using namespace Hyprutils::CLI;
CLogger::CLogger() {
m_impl = Memory::makeUnique<CLoggerImpl>(this);
}
CLogger::~CLogger() = default;
void CLogger::setLogLevel(eLogLevel level) {
m_logLevel = level;
}
void CLogger::setTime(bool enabled) {
m_impl->m_timeEnabled = enabled;
}
void CLogger::setEnableStdout(bool enabled) {
m_impl->m_stdoutEnabled = enabled;
m_impl->updateParentShouldLog();
}
void CLogger::setEnableColor(bool enabled) {
m_impl->m_colorEnabled = enabled;
}
void CLogger::setEnableRolling(bool enabled) {
m_impl->m_rollingEnabled = enabled;
}
std::expected<void, std::string> CLogger::setOutputFile(const std::string_view& file) {
if (file.empty()) {
m_impl->m_fileEnabled = false;
m_impl->m_logOfs = {};
return {};
}
std::filesystem::path filePath{file};
std::error_code ec;
if (!filePath.has_parent_path())
return std::unexpected("Path has no parent");
auto dir = filePath.parent_path();
if (!std::filesystem::exists(dir, ec) || ec)
return std::unexpected("Parent path is inaccessible, or doesn't exist");
m_impl->m_logOfs = std::ofstream{filePath, std::ios::trunc};
m_impl->m_logFilePath = filePath;
if (!m_impl->m_logOfs.good())
return std::unexpected("Failed to open a write stream");
fcntl(m_impl->m_logOfs.native_handle(), F_SETFD, FD_CLOEXEC);
m_impl->m_fileEnabled = true;
m_impl->updateParentShouldLog();
return {};
}
void CLogger::log(eLogLevel level, const std::string_view& msg) {
if (!m_shouldLogAtAll)
return;
if (level < m_logLevel)
return;
m_impl->log(level, msg);
}
const std::string& CLogger::rollingLog() {
return m_impl->m_rollingLog;
}
CLoggerImpl::CLoggerImpl(CLogger* parent) : m_parent(parent) {
updateParentShouldLog();
}
void CLoggerImpl::log(eLogLevel level, const std::string_view& msg, const std::string_view& from) {
std::lock_guard<std::mutex> lg(m_logMtx);
std::string logPrefix = "", logPrefixColor = "";
std::string logMsg = "";
switch (level) {
case LOG_TRACE:
logPrefix += "TRACE ";
logPrefixColor += "\033[1;34mTRACE \033[0m";
break;
case LOG_DEBUG:
logPrefix += "DEBUG ";
logPrefixColor += "\033[1;32mDEBUG \033[0m";
break;
case LOG_WARN:
logPrefix += "WARN ";
logPrefixColor += "\033[1;33mWARN \033[0m";
break;
case LOG_ERR:
logPrefix += "ERR ";
logPrefixColor += "\033[1;31mERR \033[0m";
break;
case LOG_CRIT:
logPrefix += "CRIT ";
logPrefixColor += "\033[1;35mCRIT \033[0m";
break;
}
if (m_timeEnabled) {
#ifndef _LIBCPP_VERSION
static auto current_zone = std::chrono::current_zone();
const auto zt = std::chrono::zoned_time{current_zone, std::chrono::system_clock::now()};
const auto hms = std::chrono::hh_mm_ss{zt.get_local_time() - std::chrono::floor<std::chrono::days>(zt.get_local_time())};
#else
// TODO: current clang 17 does not support `zoned_time`, remove this once clang 19 is ready
const auto hms = std::chrono::hh_mm_ss{std::chrono::system_clock::now() - std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now())};
#endif
logMsg += std::format("@ {} ", hms);
}
if (!from.empty()) {
logMsg += "from ";
logMsg += from;
logMsg += " ";
}
logMsg += "]: ";
logMsg += msg;
if (m_stdoutEnabled) {
try {
std::println("{}{}", m_colorEnabled ? logPrefixColor : logPrefix, logMsg);
std::fflush(stdout);
} catch (std::exception& e) {
; // this could be e.g. stdout closed
}
}
if (m_fileEnabled)
m_logOfs << logPrefix << logMsg << "\n";
if (m_rollingEnabled)
appendToRolling(logPrefix + logMsg);
}
void CLoggerImpl::updateParentShouldLog() {
m_parent->m_shouldLogAtAll = m_fileEnabled || m_stdoutEnabled;
}
void CLoggerImpl::appendToRolling(const std::string& s) {
constexpr const size_t ROLLING_LOG_SIZE = 4096;
if (!m_rollingLog.empty())
m_rollingLog += "\n";
m_rollingLog += s;
if (m_rollingLog.size() > ROLLING_LOG_SIZE)
m_rollingLog = m_rollingLog.substr(m_rollingLog.find('\n', m_rollingLog.size() - ROLLING_LOG_SIZE) + 1);
}
CLoggerConnection::CLoggerConnection(CLogger& logger) : m_impl(logger.m_impl), m_logger(&logger), m_logLevel(logger.m_logLevel) {
;
}
CLoggerConnection::~CLoggerConnection() = default;
void CLoggerConnection::setName(const std::string_view& name) {
m_name = name;
}
void CLoggerConnection::setLogLevel(eLogLevel level) {
m_logLevel = level;
}
void CLoggerConnection::log(eLogLevel level, const std::string_view& msg) {
if (!m_impl || !m_logger)
return;
if (!m_logger->m_shouldLogAtAll)
return;
if (level < m_logLevel)
return;
m_impl->log(level, msg, m_name);
}
CLogger* CLoggerConnection::getLogger() {
if (!m_impl)
return nullptr;
return m_logger;
}
void CLoggerConnection::redirect(CLogger& logger) {
m_impl = logger.m_impl;
m_logger = &logger;
}

35
src/cli/Logger.hpp Normal file
View file

@ -0,0 +1,35 @@
#include <hyprutils/cli/Logger.hpp>
#include <fstream>
#include <filesystem>
#include <mutex>
namespace Hyprutils::CLI {
class CLoggerImpl {
public:
CLoggerImpl(CLogger*);
~CLoggerImpl() = default;
CLoggerImpl(const CLoggerImpl&) = delete;
CLoggerImpl(CLoggerImpl&) = delete;
CLoggerImpl(CLoggerImpl&&) = delete;
void updateParentShouldLog();
void appendToRolling(const std::string& s);
void log(eLogLevel level, const std::string_view& msg, const std::string_view& from = "");
std::string m_rollingLog;
std::ofstream m_logOfs;
std::filesystem::path m_logFilePath;
bool m_timeEnabled = false;
bool m_stdoutEnabled = true;
bool m_fileEnabled = false;
bool m_colorEnabled = true;
bool m_rollingEnabled = false;
std::mutex m_logMtx;
// this is fine because CLogger is NOMOVE and NOCOPY
CLogger* m_parent = nullptr;
};
}

165
src/i18n/I18nEngine.cpp Normal file
View file

@ -0,0 +1,165 @@
#include "I18nEngine.hpp"
#include <algorithm>
#include <format>
#include <locale>
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::I18n;
using namespace Hyprutils;
using namespace Hyprutils::Utils;
CI18nEngine::CI18nEngine() : m_impl(Memory::makeUnique<SI18nEngineImpl>()) {
;
}
CI18nEngine::~CI18nEngine() = default;
void CI18nEngine::registerEntry(const std::string& locale, uint64_t key, std::string&& translationUTF8) {
auto& entryVec = m_impl->entries[locale];
if (entryVec.size() <= key)
entryVec.resize(key + 1);
entryVec[key].entry = std::move(translationUTF8);
entryVec[key].exists = true;
}
void CI18nEngine::registerEntry(const std::string& locale, uint64_t key, translationFn&& translationFn) {
auto& entryVec = m_impl->entries[locale];
if (entryVec.size() <= key)
entryVec.resize(key + 1);
entryVec[key].fn = std::move(translationFn);
entryVec[key].exists = true;
}
void CI18nEngine::setFallbackLocale(const std::string& locale) {
m_impl->fallbackLocale = locale;
}
std::string CI18nEngine::localizeEntry(const std::string& locale, uint64_t key, const translationVarMap& map) {
SI18nTranslationEntry* entry = nullptr;
if (m_impl->entries.contains(locale) && m_impl->entries[locale].size() > key)
entry = &m_impl->entries[locale][key];
if (locale.contains('_')) {
if (!entry || !entry->exists) {
// try to fall back to lang_LANG
auto stem = locale.substr(0, locale.find('_'));
auto stemUpper = stem;
std::ranges::transform(stemUpper, stemUpper.begin(), ::toupper);
auto newLocale = std::format("{}_{}", stem, stemUpper);
if (m_impl->entries.contains(newLocale) && m_impl->entries[newLocale].size() > key)
entry = &m_impl->entries[newLocale][key];
}
if (!entry || !entry->exists) {
// try to fall back to any lang prefixed with our prefix
const auto stem = locale.substr(0, locale.find('_') + 1);
const auto stemRaw = locale.substr(0, locale.find('_'));
for (const auto& [k, v] : m_impl->entries) {
if (k.starts_with(stem) || k == stemRaw) {
if (m_impl->entries.contains(k) && m_impl->entries[k].size() > key)
entry = &m_impl->entries[k][key];
if (entry && entry->exists)
break;
}
}
}
} else {
// locale doesn't have a _, e.g. pl
// find any locale that has the same stem
for (const auto& [k, v] : m_impl->entries) {
if (k.starts_with(locale + "_") || k == locale) {
if (m_impl->entries.contains(k) && m_impl->entries[k].size() > key)
entry = &m_impl->entries[k][key];
if (entry && entry->exists)
break;
}
}
}
if (!entry || !entry->exists) {
// fall back to general fallback
if (m_impl->entries.contains(m_impl->fallbackLocale) && m_impl->entries[m_impl->fallbackLocale].size() > key)
entry = &m_impl->entries[m_impl->fallbackLocale][key];
}
if (!entry || !entry->exists)
return "";
std::string_view rawStr = entry->entry;
std::string fnStringContainer;
if (entry->fn) {
fnStringContainer = entry->fn(map);
rawStr = fnStringContainer;
}
struct SRange {
size_t begin = 0;
size_t end = 0;
const std::string* val = nullptr;
};
std::vector<SRange> rangesFound;
// discover all replacable ranges
for (const auto& [k, v] : map) {
size_t start = rawStr.find(k, 0);
while (start != std::string::npos) {
if (start == 0 || start + k.size() >= rawStr.size())
break;
// always move the pointer
CScopeGuard x([&start, &rawStr, &k] { start = rawStr.find(k, start + 1); });
if (rawStr[start - 1] != '{' || rawStr[start + k.size()] != '}')
continue;
// add range
rangesFound.emplace_back(SRange{.begin = start - 1, .end = start + k.size() + 1, .val = &v});
}
}
if (rangesFound.empty())
return std::string{rawStr};
// build the new string. First, sort our entries
std::ranges::sort(rangesFound, [](const auto& a, const auto& b) { return a.begin < b.begin; });
// calc the size
size_t stringLen = 0;
size_t lastBegin = 0;
for (const auto& r : rangesFound) {
stringLen += r.begin - lastBegin + r.val->size();
lastBegin = r.end;
}
stringLen += rawStr.size() - lastBegin;
lastBegin = 0;
const auto ORIGINAL_STR = std::string_view{rawStr};
std::string newStr;
newStr.reserve(stringLen);
for (const auto& r : rangesFound) {
newStr += ORIGINAL_STR.substr(lastBegin, r.begin - lastBegin);
newStr += *r.val;
lastBegin = r.end;
}
newStr += ORIGINAL_STR.substr(lastBegin);
return newStr;
}
CI18nLocale CI18nEngine::getSystemLocale() {
try {
return CI18nLocale(std::locale("").name());
} catch (...) { return CI18nLocale("en_US.UTF-8"); }
}

19
src/i18n/I18nEngine.hpp Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#include <hyprutils/i18n/I18nEngine.hpp>
#include <vector>
namespace Hyprutils::I18n {
struct SI18nTranslationEntry {
bool exists = false;
std::string entry = "";
translationFn fn = nullptr;
};
struct SI18nEngineImpl {
std::unordered_map<std::string, std::vector<SI18nTranslationEntry>> entries;
std::string fallbackLocale = "en_US";
};
std::string extractLocale(std::string locale);
};

44
src/i18n/I18nLocale.cpp Normal file
View file

@ -0,0 +1,44 @@
#include "I18nEngine.hpp"
using namespace Hyprutils::I18n;
std::string Hyprutils::I18n::extractLocale(std::string locale) {
// localeStr is very arbitrary... from my testing, it can be:
// en_US.UTF-8
// LC_CTYPE=en_US
// POSIX
// *
//
// We only return e.g. en_US or pl_PL, or pl
if (locale == "POSIX")
return "en_US";
if (locale == "*")
return "en_US";
if (locale.contains('='))
locale = locale.substr(locale.find('=') + 1);
if (locale.contains('.'))
locale = locale.substr(0, locale.find('.'));
return locale;
}
CI18nLocale::CI18nLocale(std::string fullLocale) : m_rawFullLocale(std::move(fullLocale)) {
m_locale = extractLocale(m_rawFullLocale);
}
std::string CI18nLocale::locale() {
return m_locale;
}
std::string CI18nLocale::stem() {
if (m_locale.contains('_'))
return m_locale.substr(0, m_locale.find('_'));
return m_locale;
}
std::string CI18nLocale::full() {
return m_rawFullLocale;
}

View file

@ -171,7 +171,7 @@ CBox Hyprutils::Math::CBox::intersection(const CBox& other) const {
} }
bool Hyprutils::Math::CBox::overlaps(const CBox& other) const { bool Hyprutils::Math::CBox::overlaps(const CBox& other) const {
return (other.x + other.w >= x) && (x + w >= other.x) && (other.y + other.h >= y) && (y + h >= other.y); return (other.x + other.w > x) && (x + w > other.x) && (other.y + other.h > y) && (y + h > other.y);
} }
bool Hyprutils::Math::CBox::inside(const CBox& bound) const { bool Hyprutils::Math::CBox::inside(const CBox& bound) const {

View file

@ -145,18 +145,21 @@ CRegion& Hyprutils::Math::CRegion::scale(const Vector2D& scale) {
if (scale == Vector2D{1, 1}) if (scale == Vector2D{1, 1})
return *this; return *this;
auto rects = getRects(); int rectsNum = 0;
auto RECTSARR = pixman_region32_rectangles(&m_rRegion, &rectsNum);
clear(); std::vector<pixman_box32_t> boxes;
boxes.resize(rectsNum);
for (auto& r : rects) { for (int i = 0; i < rectsNum; ++i) {
r.x1 = std::floor(r.x1 * scale.x); boxes[i].x1 = std::floor(RECTSARR[i].x1 * scale.x);
r.y1 = std::floor(r.y1 * scale.y); boxes[i].x2 = std::ceil(RECTSARR[i].x2 * scale.x);
r.x2 = std::ceil(r.x2 * scale.x); boxes[i].y1 = std::floor(RECTSARR[i].y1 * scale.y);
r.y2 = std::ceil(r.y2 * scale.y); boxes[i].y2 = std::ceil(RECTSARR[i].y2 * scale.y);
add(&r);
} }
pixman_region32_fini(&m_rRegion);
pixman_region32_init_rects(&m_rRegion, boxes.data(), boxes.size());
return *this; return *this;
} }

View file

@ -5,21 +5,6 @@
#include <cmath> #include <cmath>
using namespace Hyprutils::Math; using namespace Hyprutils::Math;
using namespace Hyprutils::Memory;
Hyprutils::Math::Vector2D::Vector2D(double xx, double yy) : x(xx), y(yy) {
;
}
Hyprutils::Math::Vector2D::Vector2D(int xx, int yy) : x(sc<double>(xx)), y(sc<double>(yy)) {
;
}
Hyprutils::Math::Vector2D::Vector2D() : x(0), y(0) {
;
}
Hyprutils::Math::Vector2D::~Vector2D() {}
double Hyprutils::Math::Vector2D::normalize() { double Hyprutils::Math::Vector2D::normalize() {
// get max abs // get max abs

20
src/os/File.cpp Normal file
View file

@ -0,0 +1,20 @@
#include <hyprutils/os/File.hpp>
#include <filesystem>
#include <fstream>
using namespace Hyprutils;
using namespace Hyprutils::File;
std::expected<std::string, std::string> File::readFileAsString(const std::string_view& path) {
std::error_code ec;
if (!std::filesystem::exists(path, ec) || ec)
return std::unexpected("File not found");
std::ifstream file(std::string{path});
if (!file.good())
return std::unexpected("Failed to open file");
return std::string((std::istreambuf_iterator<char>(file)), (std::istreambuf_iterator<char>()));
}

View file

@ -49,8 +49,8 @@ bool Hyprutils::OS::CProcess::runSync() {
if (pid == -1) { if (pid == -1) {
close(outPipe[0]); close(outPipe[0]);
close(outPipe[1]); close(outPipe[1]);
close(outPipe[0]); close(errPipe[0]);
close(outPipe[1]); close(errPipe[1]);
return false; return false;
} }
@ -66,7 +66,7 @@ bool Hyprutils::OS::CProcess::runSync() {
std::vector<char*> argsC; std::vector<char*> argsC;
argsC.emplace_back(strdup(m_impl->binary.c_str())); argsC.emplace_back(strdup(m_impl->binary.c_str()));
for (auto& arg : m_impl->args) { for (auto& arg : m_impl->args) {
// TODO: does this leak? Can we just pipe c_str() as the strings won't be realloc'd? // Arguments are duplicated for execvp; if execvp fails, they must be freed.
argsC.emplace_back(strdup(arg.c_str())); argsC.emplace_back(strdup(arg.c_str()));
} }
@ -78,7 +78,12 @@ bool Hyprutils::OS::CProcess::runSync() {
} }
execvp(m_impl->binary.c_str(), argsC.data()); execvp(m_impl->binary.c_str(), argsC.data());
exit(1); for (auto ptr : argsC) {
if (ptr)
free(ptr);
}
_exit(1);
} else { } else {
// parent // parent
close(outPipe[1]); close(outPipe[1]);
@ -227,7 +232,14 @@ bool Hyprutils::OS::CProcess::runAsync() {
} }
execvp(m_impl->binary.c_str(), argsC.data()); execvp(m_impl->binary.c_str(), argsC.data());
_exit(0);
for (auto ptr : argsC) {
if (ptr)
free(ptr);
}
_exit(1);
} }
close(socket[0]); close(socket[0]);
if (write(socket[1], &grandchild, sizeof(grandchild)) != sizeof(grandchild)) { if (write(socket[1], &grandchild, sizeof(grandchild)) != sizeof(grandchild)) {

View file

@ -1,6 +1,7 @@
#include <hyprutils/path/Path.hpp> #include <hyprutils/path/Path.hpp>
#include <hyprutils/string/VarList.hpp> #include <hyprutils/string/VarList.hpp>
#include <filesystem> #include <filesystem>
#include <expected>
using namespace Hyprutils; using namespace Hyprutils;
@ -9,10 +10,18 @@ namespace Hyprutils::Path {
return basePath + "/hypr/" + programName + ".conf"; return basePath + "/hypr/" + programName + ".conf";
} }
std::string fullConfigPath(std::string basePath, std::string programName, std::string extension) {
return basePath + "/hypr/" + programName + "." + extension;
}
bool checkConfigExists(std::string basePath, std::string programName) { bool checkConfigExists(std::string basePath, std::string programName) {
return std::filesystem::exists(fullConfigPath(basePath, programName)); return std::filesystem::exists(fullConfigPath(basePath, programName));
} }
bool checkConfigExists(std::string basePath, std::string programName, std::string extension) {
return std::filesystem::exists(fullConfigPath(basePath, programName, extension));
}
std::optional<std::string> getHome() { std::optional<std::string> getHome() {
static const auto homeDir = getenv("HOME"); static const auto homeDir = getenv("HOME");
@ -44,32 +53,36 @@ namespace Hyprutils::Path {
using T = std::optional<std::string>; using T = std::optional<std::string>;
std::pair<T, T> findConfig(std::string programName) { std::pair<T, T> findConfig(std::string programName) {
return findConfig(programName, "conf");
}
std::pair<T, T> findConfig(std::string programName, std::string extension) {
bool xdgConfigHomeExists = false; bool xdgConfigHomeExists = false;
static const auto xdgConfigHome = getXdgConfigHome(); static const auto xdgConfigHome = getXdgConfigHome();
if (xdgConfigHome.has_value()) { if (xdgConfigHome.has_value()) {
xdgConfigHomeExists = true; xdgConfigHomeExists = true;
if (checkConfigExists(xdgConfigHome.value(), programName)) if (checkConfigExists(xdgConfigHome.value(), programName, extension))
return std::make_pair(fullConfigPath(xdgConfigHome.value(), programName), xdgConfigHome); return std::make_pair(fullConfigPath(xdgConfigHome.value(), programName, extension), xdgConfigHome);
} }
bool homeExists = false; bool homeExists = false;
static const auto home = getHome(); static const auto home = getHome();
if (home.has_value()) { if (home.has_value()) {
homeExists = true; homeExists = true;
if (checkConfigExists(home.value(), programName)) if (checkConfigExists(home.value(), programName, extension))
return std::make_pair(fullConfigPath(home.value(), programName), home); return std::make_pair(fullConfigPath(home.value(), programName, extension), home);
} }
static const auto xdgConfigDirs = getXdgConfigDirs(); static const auto xdgConfigDirs = getXdgConfigDirs();
if (xdgConfigDirs.has_value()) { if (xdgConfigDirs.has_value()) {
for (auto& dir : xdgConfigDirs.value()) { for (auto& dir : xdgConfigDirs.value()) {
if (checkConfigExists(dir, programName)) if (checkConfigExists(dir, programName, extension))
return std::make_pair(fullConfigPath(dir, programName), std::nullopt); return std::make_pair(fullConfigPath(dir, programName, extension), std::nullopt);
} }
} }
if (checkConfigExists("/etc/xdg", programName)) if (checkConfigExists("/etc/xdg", programName, extension))
return std::make_pair(fullConfigPath("/etc/xdg", programName), std::nullopt); return std::make_pair(fullConfigPath("/etc/xdg", programName, extension), std::nullopt);
if (xdgConfigHomeExists) if (xdgConfigHomeExists)
return std::make_pair(std::nullopt, xdgConfigHome); return std::make_pair(std::nullopt, xdgConfigHome);
@ -78,4 +91,52 @@ namespace Hyprutils::Path {
return std::make_pair(std::nullopt, std::nullopt); return std::make_pair(std::nullopt, std::nullopt);
} }
std::expected<std::string, std::string> resolvePath(const std::string& path, const std::string& base) {
if (path.empty())
return std::unexpected("empty path");
std::filesystem::path p(path);
// Expand leading ~ to the home directory
if (path[0] == '~') {
const auto homeDir = getenv("HOME");
if (!homeDir)
return std::unexpected("HOME environment variable not set");
std::string expanded(homeDir);
if (path.size() > 1) {
if (path[1] != '/')
return std::unexpected("invalid path: ~ must be followed by / or end of path");
expanded.append(path.substr(1));
}
p = std::filesystem::path(expanded);
}
// If still relative, anchor to base (or cwd)
if (p.is_relative()) {
std::filesystem::path baseP;
if (!base.empty()) {
baseP = std::filesystem::path(base);
if (baseP.is_relative()) {
std::error_code ec;
auto cwd = std::filesystem::current_path(ec);
if (ec)
return std::unexpected("failed to get current working directory: " + ec.message());
baseP = cwd / baseP;
}
} else {
std::error_code ec;
baseP = std::filesystem::current_path(ec);
if (ec)
return std::unexpected("failed to get current working directory: " + ec.message());
}
p = baseP / p;
}
// Lexically normalise (resolves . and .. without hitting the filesystem)
p = p.lexically_normal();
return p.string();
}
} }

View file

@ -10,33 +10,40 @@ using namespace Hyprutils::Memory;
#define WP CWeakPointer #define WP CWeakPointer
void Hyprutils::Signal::CSignalBase::emitInternal(void* args) { void Hyprutils::Signal::CSignalBase::emitInternal(void* args) {
std::vector<SP<CSignalListener>> listeners;
listeners.reserve(m_vListeners.size());
for (auto& l : m_vListeners) {
if (l.expired())
continue;
listeners.emplace_back(l.lock()); // Save, an event can destroy thisptr
const auto STATICS = m_vStaticListeners;
if (!m_vListeners.empty()) {
std::vector<SP<CSignalListener>> listeners;
listeners.reserve(m_vListeners.size());
for (const auto& l : m_vListeners) {
if (l.expired())
continue;
listeners.emplace_back(l.lock());
}
for (const auto& l : listeners) {
// if there is only one lock, it means the event is only held by the listeners
// vector and was removed during our iteration
if (l.strongRef() == 1)
continue;
l->emitInternal(args);
}
// release SPs
listeners.clear();
} }
auto statics = m_vStaticListeners; if (!STATICS.empty()) {
for (const auto& l : STATICS) {
for (auto& l : listeners) { l->emitInternal(args);
// if there is only one lock, it means the event is only held by the listeners }
// vector and was removed during our iteration
if (l.strongRef() == 1)
continue;
l->emitInternal(args);
} }
for (auto& l : statics) {
l->emitInternal(args);
}
// release SPs
listeners.clear();
// we cannot release any expired refs here as one of the listeners could've removed this object and // we cannot release any expired refs here as one of the listeners could've removed this object and
// as such we'd be doing a UAF // as such we'd be doing a UAF
} }

View file

@ -1,43 +1,27 @@
#include <ranges> #include <ranges>
#include <algorithm> #include <algorithm>
#include <hyprutils/string/String.hpp>
#include <hyprutils/string/ConstVarList.hpp> #include <hyprutils/string/ConstVarList.hpp>
using namespace Hyprutils::String; using namespace Hyprutils::String;
static std::string_view trim(const std::string_view& sv) {
if (sv.empty())
return sv;
size_t countBefore = 0;
while (countBefore < sv.length() && std::isspace(sv.at(countBefore))) {
countBefore++;
}
size_t countAfter = 0;
while (countAfter < sv.length() - countBefore && std::isspace(sv.at(sv.length() - countAfter - 1))) {
countAfter++;
}
return sv.substr(countBefore, sv.length() - countBefore - countAfter);
}
CConstVarList::CConstVarList(const std::string& in, const size_t lastArgNo, const char delim, const bool removeEmpty) : m_str(in) { CConstVarList::CConstVarList(const std::string& in, const size_t lastArgNo, const char delim, const bool removeEmpty) : m_str(in) {
if (in.empty()) if (in.empty())
return; return;
size_t idx = 0; size_t idx = 0;
size_t pos = 0; size_t pos = 0;
std::ranges::replace_if(m_str, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0); std::ranges::replace_if(m_str, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0);
for (const auto& s : m_str | std::views::split(0)) { for (const auto& s : m_str | std::views::split(0)) {
if (removeEmpty && s.empty()) if (removeEmpty && s.empty())
continue; continue;
if (++idx == lastArgNo) { if (++idx == lastArgNo) {
m_args.emplace_back(trim(in.substr(pos))); m_args.emplace_back(trim(std::string_view{m_str}.substr(pos)));
break; break;
} }
pos += s.size() + 1; pos += s.size() + 1;
m_args.emplace_back(trim(s.data())); m_args.emplace_back(trim(std::string_view{s.data()}));
} }
} }

View file

@ -22,7 +22,32 @@ std::string Hyprutils::String::trim(const std::string& in) {
return result; return result;
} }
std::string_view Hyprutils::String::trim(const std::string_view& sv) {
if (sv.empty())
return sv;
size_t countBefore = 0;
while (countBefore < sv.length() && std::isspace(sv.at(countBefore))) {
countBefore++;
}
size_t countAfter = 0;
while (countAfter < sv.length() - countBefore && std::isspace(sv.at(sv.length() - countAfter - 1))) {
countAfter++;
}
return sv.substr(countBefore, sv.length() - countBefore - countAfter);
}
std::string Hyprutils::String::trim(const char* in) {
return trim(std::string{in});
}
bool Hyprutils::String::isNumber(const std::string& str, bool allowfloat) { bool Hyprutils::String::isNumber(const std::string& str, bool allowfloat) {
return isNumber2(str, allowfloat);
}
bool Hyprutils::String::isNumber2(const std::string_view& str, bool allowfloat) {
if (str.empty()) if (str.empty())
return false; return false;
@ -67,3 +92,16 @@ void Hyprutils::String::replaceInString(std::string& string, const std::string&
pos += to.length(); pos += to.length();
} }
} }
bool Hyprutils::String::truthy(const std::string_view& in) {
if (in == "1")
return true;
if (in == "0")
return false;
std::string lower = std::string{in};
std::ranges::transform(lower, lower.begin(), ::tolower);
return lower.starts_with("true") || lower.starts_with("yes") || lower.starts_with("on");
}

View file

@ -22,7 +22,7 @@ Hyprutils::String::CVarList::CVarList(const std::string& in, const size_t lastAr
break; break;
} }
pos += s.size() + 1; pos += s.size() + 1;
m_vArgs.emplace_back(trim(s.data())); m_vArgs.emplace_back(trim(std::string{s.data()}));
} }
} }

126
src/string/VarList2.cpp Normal file
View file

@ -0,0 +1,126 @@
#include <algorithm>
#include <hyprutils/string/VarList2.hpp>
#include <hyprutils/string/String.hpp>
using namespace Hyprutils::String;
CVarList2::CVarList2(std::string&& in, const size_t lastArgNo, const char delim, const bool removeEmpty, const bool allowEscape) :
m_inStringCopy(std::move(in)), m_inString(m_inStringCopy) {
construct(m_inString, lastArgNo, delim, removeEmpty, allowEscape);
}
CVarList2::CVarList2(std::string_view in, const size_t lastArgNo, const char delim, const bool removeEmpty, const bool allowEscape) : m_inString(in) {
construct(m_inString, lastArgNo, delim, removeEmpty, allowEscape);
}
CVarList2::CVarList2(const char* in, const size_t lastArgNo, const char delim, const bool removeEmpty, const bool allowEscape) : m_inString(in) {
construct(m_inString, lastArgNo, delim, removeEmpty, allowEscape);
}
void CVarList2::construct(std::string_view in, const size_t lastArgNo, const char delim, const bool removeEmpty, const bool allowEscape) {
if (m_inString.empty())
return;
auto isDelimiter = [&delim](const char& c) { return delim == 's' ? std::isspace(c) : delim == c; };
size_t argBegin = 0;
std::vector<size_t> escapedIndices; // local to the current arg
for (size_t i = 0; i < m_inString.size(); ++i) {
const char& c = m_inString[i];
if (!isDelimiter(c))
continue;
if (allowEscape) {
// we allow escape, so this might be escaped. Check first
if (i - argBegin != 0) {
const char& previousC = m_inString[i - 1];
if (i - argBegin == 1) {
if (previousC == '\\') {
escapedIndices.emplace_back(i - argBegin - 1);
continue; // escaped
}
// fall to breaking, not escaped
} else {
const char& prevPreviousC = m_inString[i - 2];
if (previousC == '\\') {
// whether or not escaped, pop char
escapedIndices.emplace_back(i - argBegin - 1);
if (prevPreviousC != '\\') {
// escaped
continue;
}
}
// fall to breaking, not escaped, but mark the \\ to be popped
}
// fall to breaking, couldn't be escaped
}
}
// here we found a delimiter and need to break up the string (not escaped)
if (escapedIndices.empty()) {
// we didn't escape anything, so we can use inString
const auto ARG = trim(std::string_view{m_inString}.substr(argBegin, i - argBegin));
if (!ARG.empty() || !removeEmpty)
m_args.emplace_back(ARG);
} else {
// we escaped something, fixup the string, add to copies, then emplace
std::string cpy = std::string{m_inString.substr(argBegin, i - argBegin)};
for (size_t i = 0; i < escapedIndices.size(); ++i) {
cpy = cpy.substr(0, escapedIndices[i] - i) + cpy.substr(escapedIndices[i] - i + 1);
}
m_copyStrings.emplace_back(std::move(cpy));
m_args.emplace_back(trim(std::string_view{m_copyStrings.back()}));
}
// update next argBegin
argBegin = i + 1;
escapedIndices.clear();
}
// append anything left
if (argBegin < m_inString.size()) {
if (escapedIndices.empty()) {
// we didn't escape anything, so we can use inString
const auto ARG = trim(std::string_view{m_inString}.substr(argBegin, m_inString.size() - argBegin));
if (!ARG.empty() || !removeEmpty)
m_args.emplace_back(ARG);
} else {
// we escaped something, fixup the string, add to copies, then emplace
std::string cpy = std::string{m_inString.substr(argBegin, m_inString.size() - argBegin)};
for (size_t i = 0; i < escapedIndices.size(); ++i) {
cpy = cpy.substr(0, escapedIndices[i] - i) + cpy.substr(escapedIndices[i] - i + 1);
}
m_copyStrings.emplace_back(std::move(cpy));
m_args.emplace_back(trim(std::string_view{m_copyStrings.back()}));
}
}
}
std::string CVarList2::join(const std::string& joiner, size_t from, size_t to) const {
if (to == 0 || to <= from)
to = m_args.size();
std::string roll;
for (size_t i = from; i < to && i < m_args.size(); ++i) {
roll += m_args[i];
if (i + 1 < to && i + 1 < m_args.size())
roll += joiner;
}
return roll;
}
void CVarList2::append(std::string&& arg) {
m_copyStrings.emplace_back(std::move(arg));
m_args.emplace_back(m_copyStrings.back());
}
bool CVarList2::contains(const std::string& el) {
return std::ranges::any_of(m_args, [&el](const auto& e) { return e == el; });
}

View file

@ -1,9 +1,11 @@
#include <gtest/gtest.h>
#include <hyprutils/animation/AnimationConfig.hpp> #include <hyprutils/animation/AnimationConfig.hpp>
#include <hyprutils/animation/AnimationManager.hpp> #include <hyprutils/animation/AnimationManager.hpp>
#include <hyprutils/animation/AnimatedVariable.hpp> #include <hyprutils/animation/AnimatedVariable.hpp>
#include <hyprutils/memory/WeakPtr.hpp> #include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/UniquePtr.hpp> #include <hyprutils/memory/UniquePtr.hpp>
#include "shared.hpp"
#define SP CSharedPointer #define SP CSharedPointer
#define WP CWeakPointer #define WP CWeakPointer
@ -19,7 +21,7 @@ template <typename VarType>
using CAnimatedVariable = CGenericAnimatedVariable<VarType, EmtpyContext>; using CAnimatedVariable = CGenericAnimatedVariable<VarType, EmtpyContext>;
template <typename VarType> template <typename VarType>
using PANIMVAR = SP<CAnimatedVariable<VarType>>; using PANIMVAR = UP<CAnimatedVariable<VarType>>;
template <typename VarType> template <typename VarType>
using PANIMVARREF = WP<CAnimatedVariable<VarType>>; using PANIMVARREF = WP<CAnimatedVariable<VarType>>;
@ -45,44 +47,40 @@ CAnimationConfigTree animationTree;
class CMyAnimationManager : public CAnimationManager { class CMyAnimationManager : public CAnimationManager {
public: public:
void tick() { void tick() {
for (size_t i = 0; i < m_vActiveAnimatedVariables.size(); i++) { for (const auto& PAV : m_vActiveAnimatedVariables) {
const auto PAV = m_vActiveAnimatedVariables[i].lock();
if (!PAV || !PAV->ok() || !PAV->isBeingAnimated()) if (!PAV || !PAV->ok() || !PAV->isBeingAnimated())
continue; continue;
const auto SPENT = PAV->getPercent();
const auto PBEZIER = getBezier(PAV->getBezierName());
if (SPENT >= 1.f || !PAV->enabled()) {
PAV->warp(true, false);
continue;
}
const auto POINTY = PBEZIER->getYForPoint(SPENT);
switch (PAV->m_Type) { switch (PAV->m_Type) {
case eAVTypes::INT: { case eAVTypes::INT: {
auto avInt = dc<CAnimatedVariable<int>*>(PAV.get()); auto avInt = dc<CAnimatedVariable<int>*>(PAV.get());
if (!avInt) if (!avInt)
std::cout << Colors::RED << "Dynamic cast upcast failed" << Colors::RESET; std::cout << "Dynamic cast upcast failed\n";
const auto DELTA = avInt->goal() - avInt->value(); avInt->update();
avInt->value() = avInt->begun() + (DELTA * POINTY);
} break; } break;
case eAVTypes::TEST: { case eAVTypes::TEST: {
auto avCustom = dc<CAnimatedVariable<SomeTestType>*>(PAV.get()); auto avCustom = dc<CAnimatedVariable<SomeTestType>*>(PAV.get());
if (!avCustom) if (!avCustom)
std::cout << Colors::RED << "Dynamic cast upcast failed" << Colors::RESET; std::cout << "Dynamic cast upcast failed\n";
if (SPENT >= 1.f) if (!PAV->enabled()) {
avCustom->value().done = true; PAV->warp(true, false);
continue;
}
const auto STEP = PAV->getCurveStep();
if (STEP.finished) {
PAV->warp(true, false);
continue;
}
PAV->onUpdate();
} break; } break;
default: { default: {
std::cout << Colors::RED << "What are we even doing?" << Colors::RESET; std::cout << "What are we even doing?\n";
} break; } break;
} }
PAV->onUpdate();
} }
tickDone(); tickDone();
@ -91,11 +89,10 @@ class CMyAnimationManager : public CAnimationManager {
template <typename VarType> template <typename VarType>
void createAnimation(const VarType& v, PANIMVAR<VarType>& av, const std::string& animationConfigName) { void createAnimation(const VarType& v, PANIMVAR<VarType>& av, const std::string& animationConfigName) {
constexpr const eAVTypes EAVTYPE = std::is_same_v<VarType, int> ? eAVTypes::INT : eAVTypes::TEST; constexpr const eAVTypes EAVTYPE = std::is_same_v<VarType, int> ? eAVTypes::INT : eAVTypes::TEST;
const auto PAV = makeShared<CGenericAnimatedVariable<VarType, EmtpyContext>>(); av = makeUnique<CGenericAnimatedVariable<VarType, EmtpyContext>>();
PAV->create(EAVTYPE, sc<CAnimationManager*>(this), PAV, v); av->create2(EAVTYPE, sc<CAnimationManager*>(this), av, v);
PAV->setConfig(animationTree.getConfig(animationConfigName)); av->setConfig(animationTree.getConfig(animationConfigName));
av = std::move(PAV);
} }
virtual void scheduleTick() { virtual void scheduleTick() {
@ -121,7 +118,7 @@ class Subject {
PANIMVAR<SomeTestType> m_iC; PANIMVAR<SomeTestType> m_iC;
}; };
int config() { static int config() {
pAnimationManager = makeUnique<CMyAnimationManager>(); pAnimationManager = makeUnique<CMyAnimationManager>();
int ret = 0; int ret = 0;
@ -145,30 +142,30 @@ int config() {
auto internalCfg = animationTree.getConfig("internal"); auto internalCfg = animationTree.getConfig("internal");
// internal is a root node and should point to itself // internal is a root node and should point to itself
EXPECT(internalCfg->pParentAnimation.get(), internalCfg.get()); EXPECT_EQ(internalCfg->pParentAnimation.get(), internalCfg.get());
EXPECT(internalCfg->pValues.get(), internalCfg.get()); EXPECT_EQ(internalCfg->pValues.get(), internalCfg.get());
animationTree.setConfigForNode("global", 1, 4.0, "default", "asdf"); animationTree.setConfigForNode("global", 1, 4.0, "default", "asdf");
EXPECT(barCfg->internalEnabled, -1); EXPECT_EQ(barCfg->internalEnabled, -1);
{ {
const auto PVALUES = barCfg->pValues.lock(); const auto PVALUES = barCfg->pValues.lock();
EXPECT(PVALUES->internalEnabled, 1); EXPECT_EQ(PVALUES->internalEnabled, 1);
EXPECT(PVALUES->internalBezier, "default"); EXPECT_EQ(PVALUES->internalBezier, "default");
EXPECT(PVALUES->internalStyle, "asdf"); EXPECT_EQ(PVALUES->internalStyle, "asdf");
EXPECT(PVALUES->internalSpeed, 4.0); EXPECT_EQ(PVALUES->internalSpeed, 4.0);
} }
EXPECT(barCfg->pParentAnimation.get(), animationTree.getConfig("default").get()); EXPECT_EQ(barCfg->pParentAnimation.get(), animationTree.getConfig("default").get());
// Overwrite our own values // Overwrite our own values
animationTree.setConfigForNode("bar", 1, 4.2, "test", "qwer"); animationTree.setConfigForNode("bar", 1, 4.2, "test", "qwer");
{ {
const auto PVALUES = barCfg->pValues.lock(); const auto PVALUES = barCfg->pValues.lock();
EXPECT(PVALUES->internalEnabled, 1); EXPECT_EQ(PVALUES->internalEnabled, 1);
EXPECT(PVALUES->internalBezier, "test"); EXPECT_EQ(PVALUES->internalBezier, "test");
EXPECT(PVALUES->internalStyle, "qwer"); EXPECT_EQ(PVALUES->internalStyle, "qwer");
EXPECT(PVALUES->internalSpeed, 4.2f); EXPECT_EQ(PVALUES->internalSpeed, 4.2f);
} }
// Now overwrite the parent // Now overwrite the parent
@ -177,17 +174,17 @@ int config() {
{ {
// Expecting no change // Expecting no change
const auto PVALUES = barCfg->pValues.lock(); const auto PVALUES = barCfg->pValues.lock();
EXPECT(PVALUES->internalEnabled, 1); EXPECT_EQ(PVALUES->internalEnabled, 1);
EXPECT(PVALUES->internalBezier, "test"); EXPECT_EQ(PVALUES->internalBezier, "test");
EXPECT(PVALUES->internalStyle, "qwer"); EXPECT_EQ(PVALUES->internalStyle, "qwer");
EXPECT(PVALUES->internalSpeed, 4.2f); EXPECT_EQ(PVALUES->internalSpeed, 4.2f);
} }
return ret; return ret;
} }
int main(int argc, char** argv, char** envp) { TEST(Animation, animation) {
int ret = config(); config();
animationTree.createNode("global"); animationTree.createNode("global");
animationTree.createNode("internal"); animationTree.createNode("internal");
@ -197,8 +194,8 @@ int main(int argc, char** argv, char** envp) {
Subject s(0, 0); Subject s(0, 0);
EXPECT(s.m_iA->value(), 0); EXPECT_EQ(s.m_iA->value(), 0);
EXPECT(s.m_iB->value(), 0); EXPECT_EQ(s.m_iB->value(), 0);
// Test destruction of a CAnimatedVariable // Test destruction of a CAnimatedVariable
{ {
@ -209,22 +206,22 @@ int main(int argc, char** argv, char** envp) {
// We deliberately do not tick here, to make sure the destructor removes active animated variables // We deliberately do not tick here, to make sure the destructor removes active animated variables
} }
EXPECT(pAnimationManager->shouldTickForNext(), false); EXPECT_EQ(pAnimationManager->shouldTickForNext(), false);
EXPECT(s.m_iC->value().done, false); EXPECT_EQ(s.m_iC->value().done, false);
*s.m_iA = 10; *s.m_iA = 10;
*s.m_iB = 100; *s.m_iB = 100;
*s.m_iC = SomeTestType(true); *s.m_iC = SomeTestType(true);
EXPECT(s.m_iC->value().done, false); EXPECT_EQ(s.m_iC->value().done, false);
while (pAnimationManager->shouldTickForNext()) { while (pAnimationManager->shouldTickForNext()) {
pAnimationManager->tick(); pAnimationManager->tick();
} }
EXPECT(s.m_iA->value(), 10); EXPECT_EQ(s.m_iA->value(), 10);
EXPECT(s.m_iB->value(), 100); EXPECT_EQ(s.m_iB->value(), 100);
EXPECT(s.m_iC->value().done, true); EXPECT_EQ(s.m_iC->value().done, true);
s.m_iA->setValue(0); s.m_iA->setValue(0);
s.m_iB->setValue(0); s.m_iB->setValue(0);
@ -233,30 +230,30 @@ int main(int argc, char** argv, char** envp) {
pAnimationManager->tick(); pAnimationManager->tick();
} }
EXPECT(s.m_iA->value(), 10); EXPECT_EQ(s.m_iA->value(), 10);
EXPECT(s.m_iB->value(), 100); EXPECT_EQ(s.m_iB->value(), 100);
// Test config stuff // Test config stuff
EXPECT(s.m_iA->getBezierName(), "default"); EXPECT_EQ(s.m_iA->getBezierName(), "default");
EXPECT(s.m_iA->getStyle(), "asdf"); EXPECT_EQ(s.m_iA->getStyle(), "asdf");
EXPECT(s.m_iA->enabled(), true); EXPECT_EQ(s.m_iA->enabled(), true);
animationTree.getConfig("global")->internalEnabled = 0; animationTree.getConfig("global")->internalEnabled = 0;
EXPECT(s.m_iA->enabled(), false); EXPECT_EQ(s.m_iA->enabled(), false);
*s.m_iA = 50; *s.m_iA = 50;
pAnimationManager->tick(); // Expecting a warp pAnimationManager->tick(); // Expecting a warp
EXPECT(s.m_iA->value(), 50); EXPECT_EQ(s.m_iA->value(), 50);
// Test missing pValues // Test missing pValues
animationTree.getConfig("global")->internalEnabled = 0; animationTree.getConfig("global")->internalEnabled = 0;
animationTree.getConfig("default")->pValues.reset(); animationTree.getConfig("default")->pValues.reset();
EXPECT(s.m_iA->enabled(), false); EXPECT_EQ(s.m_iA->enabled(), false);
EXPECT(s.m_iA->getBezierName(), "default"); EXPECT_EQ(s.m_iA->getBezierName(), "default");
EXPECT(s.m_iA->getStyle(), ""); EXPECT_EQ(s.m_iA->getStyle(), "");
EXPECT(s.m_iA->getPercent(), 1.f); EXPECT_EQ(s.m_iA->getPercent(), 1.f);
// Reset // Reset
animationTree.setConfigForNode("default", 1, 1, "default"); animationTree.setConfigForNode("default", 1, 1, "default");
@ -273,18 +270,18 @@ int main(int argc, char** argv, char** envp) {
s.m_iA->setValueAndWarp(42); s.m_iA->setValueAndWarp(42);
EXPECT(beginCallbackRan, 0); EXPECT_EQ(beginCallbackRan, 0);
EXPECT(updateCallbackRan, 1); EXPECT_EQ(updateCallbackRan, 1);
EXPECT(endCallbackRan, 2); // first called when setting the callback, then when warping. EXPECT_EQ(endCallbackRan, 2); // first called when setting the callback, then when warping.
*s.m_iA = 1337; *s.m_iA = 1337;
while (pAnimationManager->shouldTickForNext()) { while (pAnimationManager->shouldTickForNext()) {
pAnimationManager->tick(); pAnimationManager->tick();
} }
EXPECT(beginCallbackRan, 1); EXPECT_EQ(beginCallbackRan, 1);
EXPECT(updateCallbackRan > 2, true); EXPECT_EQ(updateCallbackRan > 2, true);
EXPECT(endCallbackRan, 3); EXPECT_EQ(endCallbackRan, 3);
std::vector<PANIMVAR<int>> vars; std::vector<PANIMVAR<int>> vars;
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
@ -296,7 +293,7 @@ int main(int argc, char** argv, char** envp) {
// test adding / removing vars during a tick // test adding / removing vars during a tick
s.m_iA->resetAllCallbacks(); s.m_iA->resetAllCallbacks();
s.m_iA->setUpdateCallback([&vars](WP<CBaseAnimatedVariable> v) { s.m_iA->setUpdateCallback([&vars](WP<CBaseAnimatedVariable> v) {
if (v.lock() != vars.back()) if (v.get() != vars.back().get())
vars.back()->warp(); vars.back()->warp();
}); });
s.m_iA->setCallbackOnEnd([&s, &vars](auto) { s.m_iA->setCallbackOnEnd([&s, &vars](auto) {
@ -311,44 +308,44 @@ int main(int argc, char** argv, char** envp) {
pAnimationManager->tick(); pAnimationManager->tick();
} }
EXPECT(s.m_iA->value(), 1000000); EXPECT_EQ(s.m_iA->value(), 1000000);
// all vars should be set to 1337 // all vars should be set to 1337
EXPECT(std::find_if(vars.begin(), vars.end(), [](const auto& v) { return v->value() != 1337; }) == vars.end(), true); EXPECT_EQ(std::find_if(vars.begin(), vars.end(), [](const auto& v) { return v->value() != 1337; }) == vars.end(), true);
// test one-time callbacks // test one-time callbacks
s.m_iA->resetAllCallbacks(); s.m_iA->resetAllCallbacks();
s.m_iA->setCallbackOnEnd([&endCallbackRan](auto) { endCallbackRan++; }, true); s.m_iA->setCallbackOnEnd([&endCallbackRan](auto) { endCallbackRan++; }, true);
EXPECT(endCallbackRan, 4); EXPECT_EQ(endCallbackRan, 4);
s.m_iA->setValueAndWarp(10); s.m_iA->setValueAndWarp(10);
EXPECT(endCallbackRan, 4); EXPECT_EQ(endCallbackRan, 4);
EXPECT(s.m_iA->value(), 10); EXPECT_EQ(s.m_iA->value(), 10);
// test warp // test warp
*s.m_iA = 3; *s.m_iA = 3;
s.m_iA->setCallbackOnEnd([&endCallbackRan](auto) { endCallbackRan++; }, false); s.m_iA->setCallbackOnEnd([&endCallbackRan](auto) { endCallbackRan++; }, false);
s.m_iA->warp(false); s.m_iA->warp(false);
EXPECT(endCallbackRan, 4); EXPECT_EQ(endCallbackRan, 4);
*s.m_iA = 4; *s.m_iA = 4;
s.m_iA->warp(true); s.m_iA->warp(true);
EXPECT(endCallbackRan, 5); EXPECT_EQ(endCallbackRan, 5);
// test getCurveValue // test getCurveValue
*s.m_iA = 0; *s.m_iA = 0;
EXPECT(s.m_iA->getCurveValue(), 0.f); EXPECT_EQ(s.m_iA->getCurveValue(), 0.f);
s.m_iA->warp(); s.m_iA->warp();
EXPECT(s.m_iA->getCurveValue(), 1.f); EXPECT_EQ(s.m_iA->getCurveValue(), 1.f);
EXPECT(endCallbackRan, 6); EXPECT_EQ(endCallbackRan, 6);
// test end callback readding the var // test end callback readding the var
*s.m_iA = 5; *s.m_iA = 5;
s.m_iA->setCallbackOnEnd([&endCallbackRan](WP<CBaseAnimatedVariable> v) { s.m_iA->setCallbackOnEnd([&endCallbackRan](WP<CBaseAnimatedVariable> v) {
endCallbackRan++; endCallbackRan++;
const auto PAV = dc<CAnimatedVariable<int>*>(v.lock().get()); const auto PAV = dc<CAnimatedVariable<int>*>(v.get());
*PAV = 10; *PAV = 10;
PAV->setCallbackOnEnd([&endCallbackRan](WP<CBaseAnimatedVariable> v) { endCallbackRan++; }); PAV->setCallbackOnEnd([&endCallbackRan](WP<CBaseAnimatedVariable> v) { endCallbackRan++; });
@ -358,22 +355,22 @@ int main(int argc, char** argv, char** envp) {
pAnimationManager->tick(); pAnimationManager->tick();
} }
EXPECT(endCallbackRan, 8); EXPECT_EQ(endCallbackRan, 8);
EXPECT(s.m_iA->value(), 10); EXPECT_EQ(s.m_iA->value(), 10);
// Test duplicate active anim vars are not allowed // Test duplicate active anim vars are not allowed
{ {
EXPECT(pAnimationManager->m_vActiveAnimatedVariables.size(), 0); EXPECT_EQ(pAnimationManager->m_vActiveAnimatedVariables.size(), 0);
PANIMVAR<int> a; PANIMVAR<int> a;
pAnimationManager->createAnimation(1, a, "default"); pAnimationManager->createAnimation(1, a, "default");
EXPECT(pAnimationManager->m_vActiveAnimatedVariables.size(), 0); EXPECT_EQ(pAnimationManager->m_vActiveAnimatedVariables.size(), 0);
*a = 10; *a = 10;
EXPECT(pAnimationManager->m_vActiveAnimatedVariables.size(), 1); EXPECT_EQ(pAnimationManager->m_vActiveAnimatedVariables.size(), 1);
*a = 20; *a = 20;
EXPECT(pAnimationManager->m_vActiveAnimatedVariables.size(), 1); EXPECT_EQ(pAnimationManager->m_vActiveAnimatedVariables.size(), 1);
a->warp(); a->warp();
EXPECT(pAnimationManager->m_vActiveAnimatedVariables.size(), 0); EXPECT_EQ(pAnimationManager->m_vActiveAnimatedVariables.size(), 0);
EXPECT(a->value(), 20); EXPECT_EQ(a->value(), 20);
} }
// Test no crash when animation manager gets destroyed // Test no crash when animation manager gets destroyed
@ -382,16 +379,61 @@ int main(int argc, char** argv, char** envp) {
pAnimationManager->createAnimation(1, a, "default"); pAnimationManager->createAnimation(1, a, "default");
*a = 10; *a = 10;
pAnimationManager.reset(); pAnimationManager.reset();
EXPECT(a->isAnimationManagerDead(), true); EXPECT_EQ(a->isAnimationManagerDead(), true);
a->setValueAndWarp(11); a->setValueAndWarp(11);
EXPECT(a->value(), 11); EXPECT_EQ(a->value(), 11);
*a = 12; *a = 12;
a->warp(); a->warp();
EXPECT(a->value(), 12); EXPECT_EQ(a->value(), 12);
*a = 13; *a = 13;
} // a gets destroyed } // a gets destroyed
EXPECT(pAnimationManager.get(), nullptr); EXPECT_EQ(pAnimationManager.get(), nullptr);
}
return ret;
TEST(Animation, springRetargetPreservesVelocity) {
config();
pAnimationManager->addSpringWithName("momentum",
{
.stiffness = 120.f,
.damping = 8.f,
.mass = 1.f,
.valueEpsilon = 0.001f,
.velocityEpsilon = 0.001f,
});
animationTree.createNode("spring_global");
animationTree.createNode("spring_velocity", "spring_global");
animationTree.setConfigForNode("spring_global", 1, 1.f, "spring:momentum");
PANIMVAR<int> av;
pAnimationManager->createAnimation(0, av, "spring_velocity");
*av = 100;
int ticks = 0;
while (av->value() < 40 && ticks++ < 300) {
pAnimationManager->tick();
}
const auto RETARGETPOINT = av->value();
EXPECT_GT(RETARGETPOINT, 20);
PANIMVAR<int> fromRest;
pAnimationManager->createAnimation(RETARGETPOINT, fromRest, "spring_velocity");
*av = 0;
*fromRest = 0;
EXPECT_EQ(av->value(), RETARGETPOINT);
pAnimationManager->tick();
EXPECT_GT(av->value(), fromRest->value());
while (pAnimationManager->shouldTickForNext() && ticks++ < 2000) {
pAnimationManager->tick();
}
EXPECT_EQ(av->value(), 0);
EXPECT_EQ(fromRest->value(), 0);
} }

View file

@ -0,0 +1,79 @@
#include <cmath>
#include <hyprutils/animation/BezierCurve.hpp>
#include <gtest/gtest.h>
using Hyprutils::Animation::CBezierCurve;
using Hyprutils::Math::Vector2D;
static void test_nonmonotonic4_clamps_out_of_range() {
// Non-monotonic curve in X
// This used to drive the step-halving search to OOB. It should now clamp
CBezierCurve curve;
std::array<Vector2D, 4> pts = {
Vector2D{0.5f, 1.0f}, // P0
Vector2D{1.0f, 1.0f}, // P1
Vector2D{0.0f, 0.0f}, // P2
Vector2D{0.5f, 0.0f} // P3
};
curve.setup4(pts);
// x > last baked x
EXPECT_EQ(std::isfinite(curve.getYForPoint(0.6f)), true);
// Far beyond range
EXPECT_EQ(std::isfinite(curve.getYForPoint(std::numeric_limits<float>::max())), true);
EXPECT_EQ(std::isfinite(curve.getYForPoint(-std::numeric_limits<float>::max())), true);
}
static void test_adjacent_baked_x_equal() {
// Curve with flat tail (X=1, Y=1)
CBezierCurve curve;
std::array<Vector2D, 4> pts = {
Vector2D{0.0f, 0.0f}, // P0
Vector2D{0.2f, 0.2f}, // P1
Vector2D{1.0f, 1.0f}, // P2
Vector2D{1.0f, 1.0f} // P3
};
curve.setup4(pts);
// Exactly at last baked X
const float y_at_end = curve.getYForPoint(1.0f);
// Slightly beyond last baked X
const float y_past_end = curve.getYForPoint(1.0001f);
EXPECT_EQ(y_at_end, 1.0f);
EXPECT_EQ(y_past_end, y_at_end);
}
static void test_all_baked_x_equal() {
// Extreme case: X is constant along the whole curve
CBezierCurve curve;
std::array<Vector2D, 4> pts = {
Vector2D{0.0f, 0.0f}, // P0
Vector2D{0.0f, 0.3f}, // P1
Vector2D{0.0f, 0.7f}, // P2
Vector2D{0.0f, 1.0f} // P3
};
curve.setup4(pts);
// Below any baked X
const float y_lo = curve.getYForPoint(-100.0f);
const float y_0 = curve.getYForPoint(0.0f);
// Above any baked X
const float y_hi = curve.getYForPoint(100.0f);
EXPECT_EQ(std::isfinite(y_lo), true);
EXPECT_EQ(std::isfinite(y_0), true);
EXPECT_EQ(std::isfinite(y_hi), true);
// For this curve Y should stay within [0,1]
EXPECT_EQ((y_lo >= 0.0f && y_lo <= 1.0f), true);
EXPECT_EQ((y_0 >= 0.0f && y_0 <= 1.0f), true);
EXPECT_EQ((y_hi >= 0.0f && y_hi <= 1.0f), true);
}
TEST(Animation, beziercurve) {
test_nonmonotonic4_clamps_out_of_range();
test_adjacent_baked_x_equal();
test_all_baked_x_equal();
}

View file

@ -0,0 +1,105 @@
#include <cli/ArgumentParser.hpp>
#include <gtest/gtest.h>
#include <print>
using namespace Hyprutils::CLI;
using namespace Hyprutils;
constexpr const char* DESC_TEST = R"#(┏ My description
--hello -h | Says hello
--hello2 | Says hello 2
--value -v [float] | Sets a valueeeeeee
--longlonglonglongintopt -l [int] | Long long
maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaan maaan man maaan man maaan
man maaan man
)#";
TEST(CLI, ArgumentParser) {
std::vector<const char*> argv = {"app", "--hello", "--value", "0.2"};
CArgumentParser parser(argv);
EXPECT_TRUE(parser.registerBoolOption("hello", "h", "Says hello"));
EXPECT_TRUE(parser.registerBoolOption("hello2", "", "Says hello 2"));
EXPECT_TRUE(parser.registerFloatOption("value", "v", "Sets a valueeeeeee"));
EXPECT_TRUE(parser.registerIntOption("longlonglonglongintopt", "l", "Long long maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan maaan man maaan man maaan man maaan man"));
auto result = parser.parse();
EXPECT_TRUE(result.has_value());
std::println("{}", parser.getDescription("My description"));
if (!result.has_value())
std::println("Error: {}", result.error());
EXPECT_EQ(parser.getBool("hello").value_or(false), true);
EXPECT_EQ(parser.getBool("hello2").value_or(false), false);
EXPECT_EQ(parser.getFloat("value").value_or(0.F), 0.2F);
EXPECT_EQ(parser.getDescription("My description"), DESC_TEST);
CArgumentParser parser2(argv);
EXPECT_TRUE(parser2.registerBoolOption("hello2", "e", "Says hello 2"));
EXPECT_TRUE(parser2.registerFloatOption("value", "v", "Sets a valueeeeeee"));
EXPECT_TRUE(parser2.registerIntOption("longlonglonglongintopt", "l", "Long long maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan maaan man maaan man maaan man maaan man"));
EXPECT_TRUE(parser2.registerFloatOption("value2", "", "Sets a valueeeeeee 2"));
EXPECT_TRUE(!parser2.registerFloatOption("", "a", "Sets a valueeeeeee 2"));
auto result2 = parser2.parse();
EXPECT_TRUE(!result2.has_value());
std::vector<const char*> argv3 = {"app", "--hello", "--value"};
CArgumentParser parser3(argv3);
EXPECT_TRUE(parser3.registerBoolOption("hello2", "e", "Says hello 2"));
EXPECT_TRUE(parser3.registerFloatOption("value", "v", "Sets a valueeeeeee"));
EXPECT_TRUE(parser3.registerIntOption("longlonglonglongintopt", "l", "Long long maaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaan maaan man maaan man maaan man maaan man"));
auto result3 = parser3.parse();
EXPECT_TRUE(!result3.has_value());
std::vector<const char*> argv4 = {"app", "--value", "hi", "-w", "2"};
CArgumentParser parser4(argv4);
EXPECT_TRUE(parser4.registerStringOption("value", "v", "Sets a valueeeeeee"));
EXPECT_TRUE(parser4.registerIntOption("value2", "w", "Sets a valueeeeeee 2"));
auto result4 = parser4.parse();
EXPECT_TRUE(result4.has_value());
EXPECT_EQ(parser4.getString("value").value_or(""), "hi");
EXPECT_EQ(parser4.getInt("value2").value_or(0), 2);
std::vector<const char*> argv5 = {
"app",
"e",
};
CArgumentParser parser5(argv5);
EXPECT_TRUE(parser5.registerStringOption("value", "v", "Sets a valueeeeeee"));
EXPECT_TRUE(parser5.registerStringOption("value2", "w", "Sets a valueeeeeee 2"));
auto result5 = parser5.parse();
EXPECT_TRUE(!result5.has_value());
CArgumentParser parser6(argv5);
EXPECT_TRUE(parser6.registerStringOption("aa", "v", "Sets a valueeeeeee"));
EXPECT_TRUE(!parser6.registerStringOption("aa", "w", "Sets a valueeeeeee 2"));
EXPECT_TRUE(parser6.registerStringOption("bb", "b", "Sets a valueeeeeee"));
EXPECT_TRUE(!parser6.registerStringOption("cc", "b", "Sets a valueeeeeee 2"));
}

104
tests/cli/Logger.cpp Normal file
View file

@ -0,0 +1,104 @@
#include <cli/Logger.hpp>
#include <hyprutils/os/File.hpp>
#include <gtest/gtest.h>
#include <filesystem>
using namespace Hyprutils::CLI;
using namespace Hyprutils;
TEST(CLI, Logger) {
CLogger logger;
logger.setEnableRolling(true);
logger.log(Hyprutils::CLI::LOG_DEBUG, "Hello!");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!");
logger.log(Hyprutils::CLI::LOG_TRACE, "Hello!");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!");
logger.setLogLevel(LOG_TRACE);
logger.log(Hyprutils::CLI::LOG_TRACE, "Hello, {}!", "Trace");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!");
CLoggerConnection connection(logger);
connection.setName("conn");
connection.log(Hyprutils::CLI::LOG_TRACE, "Hello from connection!");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!");
connection.setLogLevel(Hyprutils::CLI::LOG_WARN);
connection.log(Hyprutils::CLI::LOG_DEBUG, "Hello from connection!");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!");
logger.setEnableRolling(false);
connection.log(Hyprutils::CLI::LOG_ERR, "Err!");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!");
logger.setEnableStdout(false);
logger.log(Hyprutils::CLI::LOG_ERR, "Error");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!");
auto res = logger.setOutputFile("./loggerFile.log");
EXPECT_TRUE(res);
logger.log(LOG_DEBUG, "Hi file!");
res = logger.setOutputFile(""); // clear
EXPECT_TRUE(res);
auto fileRead = File::readFileAsString("./loggerFile.log");
EXPECT_TRUE(fileRead);
EXPECT_EQ(fileRead.value_or(""), "DEBUG ]: Hi file!\n");
std::error_code ec;
std::filesystem::remove("./loggerFile.log", ec);
// TODO: maybe find a way to test the times and color?
logger.setEnableStdout(true);
logger.setTime(true);
logger.log(Hyprutils::CLI::LOG_WARN, "Timed warning!");
logger.setEnableColor(false);
logger.log(Hyprutils::CLI::LOG_CRIT, "rip");
logger.setEnableRolling(true);
// spam some logs to check rolling
for (size_t i = 0; i < 200; ++i) {
logger.log(LOG_ERR, "Oh noes!!!");
}
EXPECT_TRUE(logger.rollingLog().size() < 4096);
EXPECT_TRUE(logger.rollingLog().starts_with("ERR")); // test the breaking is done correctly
// test scoping
CLogger* pLogger = new CLogger();
CLoggerConnection* pConnection = new CLoggerConnection(*pLogger);
pLogger->setEnableStdout(false);
pConnection->log(LOG_DEBUG, "This shouldn't log anything.");
EXPECT_TRUE(pLogger->rollingLog().empty());
delete pLogger;
pConnection->log(LOG_DEBUG, "This shouldn't do anything, or crash.");
}

View file

@ -1,49 +0,0 @@
#include <hyprutils/os/FileDescriptor.hpp>
#include "shared.hpp"
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
using namespace Hyprutils::OS;
int main(int argc, char** argv, char** envp) {
std::string name = "/test_filedescriptors";
CFileDescriptor fd(shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600));
int ret = 0;
EXPECT(fd.isValid(), true);
EXPECT(fd.isReadable(), true);
int flags = fd.getFlags();
EXPECT(fd.getFlags(), FD_CLOEXEC);
flags &= ~FD_CLOEXEC;
fd.setFlags(flags);
EXPECT(fd.getFlags(), !FD_CLOEXEC);
CFileDescriptor fd2 = fd.duplicate();
EXPECT(fd.isValid(), true);
EXPECT(fd.isReadable(), true);
EXPECT(fd2.isValid(), true);
EXPECT(fd2.isReadable(), true);
CFileDescriptor fd3(fd2.take());
EXPECT(fd.isValid(), true);
EXPECT(fd.isReadable(), true);
EXPECT(fd2.isValid(), false);
EXPECT(fd2.isReadable(), false);
// .duplicate default flags is FD_CLOEXEC
EXPECT(fd3.getFlags(), FD_CLOEXEC);
fd.reset();
fd2.reset();
fd3.reset();
EXPECT(fd.isReadable(), false);
EXPECT(fd2.isReadable(), false);
EXPECT(fd3.isReadable(), false);
shm_unlink(name.c_str());
return ret;
}

80
tests/i18n/Engine.cpp Normal file
View file

@ -0,0 +1,80 @@
#include <i18n/I18nEngine.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::I18n;
enum eTxtKeys : uint64_t {
TXT_KEY_HELLO,
TXT_KEY_I_HAVE_APPLES,
TXT_KEY_FALLBACK,
};
TEST(I18n, Engine) {
CI18nEngine engine;
engine.setFallbackLocale("en_US");
engine.registerEntry("en_US", TXT_KEY_HELLO, "Hello World!");
engine.registerEntry("en_US", TXT_KEY_I_HAVE_APPLES, [](const translationVarMap& m) {
if (std::stoi(m.at("count")) == 1)
return "I have {count} apple.";
else
return "I have {count} apples.";
});
engine.registerEntry("en_US", TXT_KEY_FALLBACK, "Fallback string!");
engine.registerEntry("pl_PL", TXT_KEY_HELLO, "Witaj świecie!");
engine.registerEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, [](const translationVarMap& m) {
const auto COUNT = std::stoi(m.at("count"));
if (COUNT == 1)
return "Mam {count} jabłko.";
else if (COUNT < 5)
return "Mam {count} jabłka.";
else
return "Mam {count} jabłek.";
});
engine.registerEntry("es_XX", TXT_KEY_FALLBACK, "I don't speak spanish");
engine.registerEntry("es_ES", TXT_KEY_FALLBACK, "I don't speak spanish here either");
engine.registerEntry("ts_TST", TXT_KEY_FALLBACK, "Hello {var1} world {var2}");
engine.registerEntry("am", TXT_KEY_FALLBACK, "Amongus!");
EXPECT_EQ(engine.localizeEntry("en_US", TXT_KEY_HELLO, {}), "Hello World!");
EXPECT_EQ(engine.localizeEntry("pl_PL", TXT_KEY_HELLO, {}), "Witaj świecie!");
EXPECT_EQ(engine.localizeEntry("de_DE", TXT_KEY_HELLO, {}), "Hello World!");
EXPECT_EQ(engine.localizeEntry("en_US", TXT_KEY_I_HAVE_APPLES, {{"count", "1"}}), "I have 1 apple.");
EXPECT_EQ(engine.localizeEntry("en_US", TXT_KEY_I_HAVE_APPLES, {{"count", "2"}}), "I have 2 apples.");
EXPECT_EQ(engine.localizeEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, {{"count", "1"}}), "Mam 1 jabłko.");
EXPECT_EQ(engine.localizeEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, {{"count", "2"}}), "Mam 2 jabłka.");
EXPECT_EQ(engine.localizeEntry("pl_PL", TXT_KEY_I_HAVE_APPLES, {{"count", "5"}}), "Mam 5 jabłek.");
EXPECT_EQ(engine.localizeEntry("pl", TXT_KEY_I_HAVE_APPLES, {{"count", "5"}}), "Mam 5 jabłek.");
EXPECT_EQ(engine.localizeEntry("pl_XX", TXT_KEY_I_HAVE_APPLES, {{"count", "5"}}), "Mam 5 jabłek.");
EXPECT_EQ(engine.localizeEntry("en_XX", TXT_KEY_I_HAVE_APPLES, {{"count", "2"}}), "I have 2 apples.");
EXPECT_EQ(engine.localizeEntry("es_YY", TXT_KEY_FALLBACK, {}), "I don't speak spanish here either");
EXPECT_EQ(engine.localizeEntry("es_XX", TXT_KEY_FALLBACK, {}), "I don't speak spanish");
EXPECT_EQ(engine.localizeEntry("pl_PL", TXT_KEY_FALLBACK, {}), "Fallback string!");
EXPECT_EQ(engine.localizeEntry("am_AM", TXT_KEY_FALLBACK, {}), "Amongus!");
// test weird translations
engine.registerEntry("ts", TXT_KEY_HELLO, "count}");
EXPECT_EQ(engine.localizeEntry("ts", TXT_KEY_HELLO, {{"count", "1"}}), "count}");
engine.registerEntry("ts", TXT_KEY_HELLO, "{count");
EXPECT_EQ(engine.localizeEntry("ts", TXT_KEY_HELLO, {{"count", "1"}}), "{count");
EXPECT_EQ(engine.localizeEntry("ts", 42069 /* invalid key */, {{"count", "1"}}), "");
EXPECT_EQ(engine.localizeEntry("ts_TST", TXT_KEY_FALLBACK, {{"var1", "hi"}, {"var2", "!"}}), "Hello hi world !");
// Order shouldn't matter
EXPECT_EQ(engine.localizeEntry("ts_TST", TXT_KEY_FALLBACK, {{"var2", "!"}, {"var1", "hi"}}), "Hello hi world !");
}

11
tests/i18n/Locale.cpp Normal file
View file

@ -0,0 +1,11 @@
#include <gtest/gtest.h>
#include <i18n/I18nEngine.hpp>
using namespace Hyprutils::I18n;
TEST(I18n, Locale) {
EXPECT_EQ(extractLocale("pl_PL.UTF-8"), "pl_PL");
EXPECT_EQ(extractLocale("POSIX"), "en_US");
EXPECT_EQ(extractLocale("*"), "en_US");
EXPECT_EQ(extractLocale("LC_CTYPE=pl_PL.UTF-8"), "pl_PL");
}

View file

@ -1,122 +0,0 @@
#include <hyprutils/math/Region.hpp>
#include <hyprutils/math/Mat3x3.hpp>
#include "shared.hpp"
using namespace Hyprutils::Math;
int main(int argc, char** argv, char** envp) {
CRegion rg = {0, 0, 100, 100};
rg.add(CBox{{}, {20, 200}});
int ret = 0;
EXPECT(rg.getExtents().height, 200);
EXPECT(rg.getExtents().width, 100);
rg.intersect(CBox{10, 10, 300, 300});
EXPECT(rg.getExtents().width, 90);
EXPECT(rg.getExtents().height, 190);
/*Box.cpp test cases*/
// Test default constructor and accessors
{
CBox box1;
EXPECT(box1.x, 0);
EXPECT(box1.y, 0);
EXPECT(box1.width, 0);
EXPECT(box1.height, 0);
// Test parameterized constructor and accessors
CBox box2(10, 20, 30, 40);
EXPECT(box2.x, 10);
EXPECT(box2.y, 20);
EXPECT(box2.width, 30);
EXPECT(box2.height, 40);
// Test setters and getters
box2.translate(Vector2D(5, -5));
EXPECT_VECTOR2D(box2.pos(), Vector2D(15, 15));
}
//Test Scaling and Transformation
{
CBox box(10, 10, 20, 30);
// Test scaling
box.scale(2.0);
EXPECT_VECTOR2D(box.size(), Vector2D(40, 60));
EXPECT_VECTOR2D(box.pos(), Vector2D(20, 20));
// Test scaling from center
box.scaleFromCenter(0.5);
EXPECT_VECTOR2D(box.size(), Vector2D(20, 30));
EXPECT_VECTOR2D(box.pos(), Vector2D(30, 35));
// Test transformation
box.transform(HYPRUTILS_TRANSFORM_90, 100, 200);
EXPECT_VECTOR2D(box.pos(), Vector2D(135, 30));
EXPECT_VECTOR2D(box.size(), Vector2D(30, 20));
// Test Intersection and Extents
}
{
CBox box1(0, 0, 100, 100);
CBox box2(50, 50, 100, 100);
CBox intersection = box1.intersection(box2);
EXPECT_VECTOR2D(intersection.pos(), Vector2D(50, 50));
EXPECT_VECTOR2D(intersection.size(), Vector2D(50, 50));
SBoxExtents extents = box1.extentsFrom(box2);
EXPECT_VECTOR2D(extents.topLeft, Vector2D(50, 50));
EXPECT_VECTOR2D(extents.bottomRight, Vector2D(-50, -50));
}
// Test Boundary Conditions and Special Cases
{
CBox box(0, 0, 50, 50);
EXPECT(box.empty(), false);
EXPECT(box.containsPoint(Vector2D(25, 25)), true);
EXPECT(box.containsPoint(Vector2D(60, 60)), false);
EXPECT(box.overlaps(CBox(25, 25, 50, 50)), true);
EXPECT(box.inside(CBox(0, 0, 100, 100)), false);
}
// Test matrices
{
Mat3x3 jeremy = Mat3x3::outputProjection({1920, 1080}, HYPRUTILS_TRANSFORM_FLIPPED_90);
Mat3x3 matrixBox = jeremy.projectBox(CBox{10, 10, 200, 200}, HYPRUTILS_TRANSFORM_NORMAL).translate({100, 100}).scale({1.25F, 1.5F}).transpose();
Mat3x3 expected = std::array<float, 9>{0, 0.46296296, 0, 0.3125, 0, 0, 19.84375, 36.055557, 1};
// we need to do this to avoid precision errors on 32-bit archs
EXPECT(std::abs(expected.getMatrix().at(0) - matrixBox.getMatrix().at(0)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(1) - matrixBox.getMatrix().at(1)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(2) - matrixBox.getMatrix().at(2)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(3) - matrixBox.getMatrix().at(3)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(4) - matrixBox.getMatrix().at(4)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(5) - matrixBox.getMatrix().at(5)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(6) - matrixBox.getMatrix().at(6)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(7) - matrixBox.getMatrix().at(7)) < 0.1, true);
EXPECT(std::abs(expected.getMatrix().at(8) - matrixBox.getMatrix().at(8)) < 0.1, true);
}
{
Vector2D original(30, 40);
Vector2D monitorSize(100, 200);
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_NORMAL, monitorSize), Vector2D(30, 40 ));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_90, monitorSize), Vector2D(40, 200 - 30));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_180, monitorSize), Vector2D(100 - 30, 200 - 40));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_270, monitorSize), Vector2D(100 - 40, 30 ));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_FLIPPED, monitorSize), Vector2D(100 - 30, 40 ));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_FLIPPED_90, monitorSize), Vector2D(40, 30 ));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_FLIPPED_180, monitorSize), Vector2D(30, 200 - 40));
EXPECT_VECTOR2D(original.transform(HYPRUTILS_TRANSFORM_FLIPPED_270, monitorSize), Vector2D(100 - 40, 200 - 30));
}
return ret;
}

73
tests/math/Box.cpp Normal file
View file

@ -0,0 +1,73 @@
#include <hyprutils/math/Box.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::Math;
TEST(Math, box) {
// Test default constructor and accessors
{
CBox box1;
EXPECT_EQ(box1.x, 0);
EXPECT_EQ(box1.y, 0);
EXPECT_EQ(box1.width, 0);
EXPECT_EQ(box1.height, 0);
// Test parameterized constructor and accessors
CBox box2(10, 20, 30, 40);
EXPECT_EQ(box2.x, 10);
EXPECT_EQ(box2.y, 20);
EXPECT_EQ(box2.width, 30);
EXPECT_EQ(box2.height, 40);
// Test setters and getters
box2.translate(Vector2D(5, -5));
EXPECT_EQ(box2.pos(), Vector2D(15, 15));
}
//Test Scaling and Transformation
{
CBox box(10, 10, 20, 30);
// Test scaling
box.scale(2.0);
EXPECT_EQ(box.size(), Vector2D(40, 60));
EXPECT_EQ(box.pos(), Vector2D(20, 20));
// Test scaling from center
box.scaleFromCenter(0.5);
EXPECT_EQ(box.size(), Vector2D(20, 30));
EXPECT_EQ(box.pos(), Vector2D(30, 35));
// Test transformation
box.transform(HYPRUTILS_TRANSFORM_90, 100, 200);
EXPECT_EQ(box.pos(), Vector2D(135, 30));
EXPECT_EQ(box.size(), Vector2D(30, 20));
// Test Intersection and Extents
}
{
CBox box1(0, 0, 100, 100);
CBox box2(50, 50, 100, 100);
CBox intersection = box1.intersection(box2);
EXPECT_EQ(intersection.pos(), Vector2D(50, 50));
EXPECT_EQ(intersection.size(), Vector2D(50, 50));
SBoxExtents extents = box1.extentsFrom(box2);
EXPECT_EQ(extents.topLeft, Vector2D(50, 50));
EXPECT_EQ(extents.bottomRight, Vector2D(-50, -50));
}
// Test Boundary Conditions and Special Cases
{
CBox box(0, 0, 50, 50);
EXPECT_EQ(box.empty(), false);
EXPECT_EQ(box.containsPoint(Vector2D(25, 25)), true);
EXPECT_EQ(box.containsPoint(Vector2D(60, 60)), false);
EXPECT_EQ(box.overlaps(CBox(25, 25, 50, 50)), true);
EXPECT_EQ(box.inside(CBox(0, 0, 100, 100)), false);
}
}

23
tests/math/Mat3x3.cpp Normal file
View file

@ -0,0 +1,23 @@
#include <hyprutils/math/Mat3x3.hpp>
#include <hyprutils/math/Box.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::Math;
TEST(Math, mat3x3) {
Mat3x3 jeremy = Mat3x3::outputProjection({1920, 1080}, HYPRUTILS_TRANSFORM_FLIPPED_90);
Mat3x3 matrixBox = jeremy.projectBox(CBox{10, 10, 200, 200}, HYPRUTILS_TRANSFORM_NORMAL).translate({100, 100}).scale({1.25F, 1.5F}).transpose();
Mat3x3 expected = std::array<float, 9>{0, 0.46296296, 0, 0.3125, 0, 0, 19.84375, 36.055557, 1};
// we need to do this to avoid precision errors on 32-bit archs
EXPECT_EQ(std::abs(expected.getMatrix().at(0) - matrixBox.getMatrix().at(0)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(1) - matrixBox.getMatrix().at(1)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(2) - matrixBox.getMatrix().at(2)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(3) - matrixBox.getMatrix().at(3)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(4) - matrixBox.getMatrix().at(4)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(5) - matrixBox.getMatrix().at(5)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(6) - matrixBox.getMatrix().at(6)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(7) - matrixBox.getMatrix().at(7)) < 0.1, true);
EXPECT_EQ(std::abs(expected.getMatrix().at(8) - matrixBox.getMatrix().at(8)) < 0.1, true);
}

18
tests/math/Region.cpp Normal file
View file

@ -0,0 +1,18 @@
#include <hyprutils/math/Region.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::Math;
TEST(Math, region) {
CRegion rg(CBox{{20, 20}, {40, 40}});
auto extents = rg.getExtents();
EXPECT_EQ(extents.pos(), Vector2D(20, 20));
EXPECT_EQ(extents.size(), Vector2D(40, 40));
rg.scale(2);
extents = rg.getExtents();
EXPECT_EQ(extents.pos(), Vector2D(40, 40));
EXPECT_EQ(extents.size(), Vector2D(80, 80));
}

19
tests/math/Vector2D.cpp Normal file
View file

@ -0,0 +1,19 @@
#include <hyprutils/math/Vector2D.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::Math;
TEST(Math, vector2d) {
Vector2D original(30, 40);
Vector2D monitorSize(100, 200);
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_NORMAL, monitorSize), Vector2D(30, 40));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_90, monitorSize), Vector2D(40, 200 - 30));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_180, monitorSize), Vector2D(100 - 30, 200 - 40));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_270, monitorSize), Vector2D(100 - 40, 30));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_FLIPPED, monitorSize), Vector2D(100 - 30, 40));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_FLIPPED_90, monitorSize), Vector2D(40, 30));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_FLIPPED_180, monitorSize), Vector2D(30, 200 - 40));
EXPECT_EQ(original.transform(HYPRUTILS_TRANSFORM_FLIPPED_270, monitorSize), Vector2D(100 - 40, 200 - 30));
}

View file

@ -1,154 +0,0 @@
#include <hyprutils/memory/UniquePtr.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/Atomic.hpp>
#include "shared.hpp"
#include <chrono>
#include <print>
#include <thread>
#include <vector>
using namespace Hyprutils::Memory;
#define SP CSharedPointer
#define WP CWeakPointer
#define UP CUniquePointer
#define ASP CAtomicSharedPointer
#define AWP CAtomicWeakPointer
#define NTHREADS 8
#define ITERATIONS 10000
static int testAtomicImpl() {
int ret = 0;
{
// Using makeShared here could lead to invalid refcounts.
ASP<int> shared = makeAtomicShared<int>(0);
std::vector<std::thread> threads;
threads.reserve(NTHREADS);
for (size_t i = 0; i < NTHREADS; i++) {
threads.emplace_back([shared]() {
for (size_t j = 0; j < ITERATIONS; j++) {
ASP<int> strongRef = shared;
(*shared)++;
strongRef.reset();
}
});
}
for (auto& thread : threads) {
thread.join();
}
// Actual count is not incremented in a thread-safe manner here, so we can't check it.
// We just want to check that the concurent refcounting doesn't cause any memory corruption.
shared.reset();
EXPECT(shared, false);
}
{
ASP<int> shared = makeAtomicShared<int>(0);
AWP<int> weak = shared;
std::vector<std::thread> threads;
threads.reserve(NTHREADS);
for (size_t i = 0; i < NTHREADS; i++) {
threads.emplace_back([weak]() {
for (size_t j = 0; j < ITERATIONS; j++) {
if (auto s = weak.lock(); s) {
(*s)++;
}
}
});
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
shared.reset();
for (auto& thread : threads) {
thread.join();
}
EXPECT(shared.strongRef(), 0);
EXPECT(weak.valid(), false);
auto shared2 = weak.lock();
EXPECT(shared, false);
EXPECT(shared2.get(), nullptr);
EXPECT(shared.strongRef(), 0);
EXPECT(weak.valid(), false);
EXPECT(weak.expired(), true);
}
{ // This tests recursive deletion. When foo will be deleted, bar will be deleted within the foo dtor.
class CFoo {
public:
AWP<CFoo> bar;
};
ASP<CFoo> foo = makeAtomicShared<CFoo>();
foo->bar = foo;
}
return ret;
}
int main(int argc, char** argv, char** envp) {
SP<int> intPtr = makeShared<int>(10);
SP<int> intPtr2 = makeShared<int>(-1337);
UP<int> intUnique = makeUnique<int>(420);
int ret = 0;
EXPECT(*intPtr, 10);
EXPECT(intPtr.strongRef(), 1);
EXPECT(*intUnique, 420);
WP<int> weak = intPtr;
WP<int> weakUnique = intUnique;
EXPECT(*intPtr, 10);
EXPECT(intPtr.strongRef(), 1);
EXPECT(*weak, 10);
EXPECT(weak.expired(), false);
EXPECT(*weakUnique, 420);
EXPECT(weakUnique.expired(), false);
EXPECT(intUnique.impl_->wref(), 1);
SP<int> sharedFromUnique = weakUnique.lock();
EXPECT(sharedFromUnique, nullptr);
std::vector<SP<int>> sps;
sps.push_back(intPtr);
sps.emplace_back(intPtr);
sps.push_back(intPtr2);
sps.emplace_back(intPtr2);
std::erase_if(sps, [intPtr](const auto& e) { return e == intPtr; });
intPtr.reset();
intUnique.reset();
EXPECT(weak.impl_->ref(), 0);
EXPECT(weakUnique.impl_->ref(), 0);
EXPECT(weakUnique.impl_->wref(), 1);
EXPECT(intPtr2.strongRef(), 3);
EXPECT(weak.expired(), true);
EXPECT(weakUnique.expired(), true);
auto intPtr2AsUint = reinterpretPointerCast<unsigned int>(intPtr2);
EXPECT(intPtr2.strongRef(), 4);
EXPECT(intPtr2AsUint.strongRef(), 4);
EXPECT(*intPtr2AsUint > 0, true);
EXPECT(*intPtr2AsUint, (unsigned int)(int)-1337);
*intPtr2AsUint = 10;
EXPECT(*intPtr2AsUint, 10);
EXPECT(*intPtr2, 10);
EXPECT(testAtomicImpl(), 0);
return ret;
}

324
tests/memory/Memory.cpp Normal file
View file

@ -0,0 +1,324 @@
#include <hyprutils/memory/Atomic.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <gtest/gtest.h>
#include <chrono>
#include <thread>
#include <vector>
using namespace Hyprutils::Memory;
#define SP CSharedPointer
#define WP CWeakPointer
#define UP CUniquePointer
#define ASP CAtomicSharedPointer
#define AWP CAtomicWeakPointer
#define NTHREADS 8
#define ITERATIONS 10000
static void testAtomicImpl() {
{
// Using makeShared here could lead to invalid refcounts.
ASP<int> shared = makeAtomicShared<int>(0);
std::vector<std::thread> threads;
threads.reserve(NTHREADS);
for (size_t i = 0; i < NTHREADS; i++) {
threads.emplace_back([shared]() {
for (size_t j = 0; j < ITERATIONS; j++) {
ASP<int> strongRef = shared;
(*shared)++;
strongRef.reset();
}
});
}
for (auto& thread : threads) {
thread.join();
}
// Actual count is not incremented in a thread-safe manner here, so we can't check it.
// We just want to check that the concurent refcounting doesn't cause any memory corruption.
shared.reset();
EXPECT_EQ(shared, false);
}
{
ASP<int> shared = makeAtomicShared<int>(0);
AWP<int> weak = shared;
std::vector<std::thread> threads;
threads.reserve(NTHREADS);
for (size_t i = 0; i < NTHREADS; i++) {
threads.emplace_back([weak]() {
for (size_t j = 0; j < ITERATIONS; j++) {
if (auto s = weak.lock(); s) {
(*s)++;
}
}
});
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
shared.reset();
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(shared.strongRef(), 0);
EXPECT_EQ(weak.valid(), false);
auto shared2 = weak.lock();
EXPECT_EQ(shared, false);
EXPECT_EQ(shared2.get(), nullptr);
EXPECT_EQ(shared.strongRef(), 0);
EXPECT_EQ(weak.valid(), false);
EXPECT_EQ(weak.expired(), true);
}
{ // This tests recursive deletion. When foo will be deleted, bar will be deleted within the foo dtor.
class CFoo {
public:
AWP<CFoo> bar;
};
ASP<CFoo> foo = makeAtomicShared<CFoo>();
foo->bar = foo;
}
{ // This tests destroying the data when storing the base class of a type
class ITest {
public:
size_t num = 0;
ITest() : num(1234) {};
};
class CA : public ITest {
public:
size_t num2 = 0;
CA() : ITest(), num2(4321) {};
};
class CB : public ITest {
public:
int num2 = 0;
CB() : ITest(), num2(-1) {};
};
ASP<ITest> genericAtomic = nullptr;
SP<ITest> genericNormal = nullptr;
{
auto derivedAtomic = makeAtomicShared<CA>();
auto derivedNormal = makeShared<CA>();
genericAtomic = derivedAtomic;
genericNormal = derivedNormal;
}
EXPECT_EQ(!!genericAtomic, true);
EXPECT_EQ(!!genericNormal, true);
}
}
class InterfaceA {
public:
virtual ~InterfaceA() = default;
int m_ifaceAInt = 69;
int m_ifaceAShit = 1;
};
class InterfaceB {
public:
virtual ~InterfaceB() = default;
int m_ifaceBInt = 2;
int m_ifaceBShit = 3;
};
class CChild : public InterfaceA, public InterfaceB {
public:
virtual ~CChild() = default;
int m_childInt = 4;
};
class CChildA : public InterfaceA {
public:
int m_childAInt = 4;
};
static void testHierarchy() {
// Same test for atomic and non-atomic
{
SP<CChildA> childA = makeShared<CChildA>();
auto ifaceA = SP<InterfaceA>(childA);
EXPECT_TRUE(ifaceA);
EXPECT_EQ(ifaceA->m_ifaceAInt, 69);
auto ifaceB = dynamicPointerCast<InterfaceA>(SP<CChildA>{});
auto ifaceB2 = dynamicPointerCast<InterfaceA>(WP<CChildA>{});
EXPECT_TRUE(!ifaceB);
EXPECT_TRUE(!ifaceB2);
}
{
SP<CChild> child = makeShared<CChild>();
SP<InterfaceA> ifaceA = dynamicPointerCast<InterfaceA>(child);
SP<InterfaceB> ifaceB = dynamicPointerCast<InterfaceB>(child);
EXPECT_TRUE(ifaceA);
EXPECT_TRUE(ifaceB);
EXPECT_EQ(ifaceA->m_ifaceAInt, 69);
EXPECT_EQ(ifaceB->m_ifaceBInt, 2);
WP<InterfaceA> ifaceAWeak = ifaceA;
WP<InterfaceB> ifaceBWeak = dynamicPointerCast<InterfaceB>(WP<CChild>{child});
child.reset();
EXPECT_TRUE(ifaceAWeak);
EXPECT_TRUE(ifaceBWeak);
EXPECT_TRUE(ifaceA);
EXPECT_EQ(ifaceAWeak->m_ifaceAInt, 69);
EXPECT_EQ(ifaceA->m_ifaceAInt, 69);
ifaceA.reset();
EXPECT_TRUE(ifaceAWeak);
EXPECT_EQ(ifaceAWeak->m_ifaceAInt, 69);
EXPECT_TRUE(ifaceB);
EXPECT_EQ(ifaceB->m_ifaceBInt, 2);
EXPECT_EQ(ifaceBWeak->m_ifaceBInt, 2);
ifaceB.reset();
EXPECT_TRUE(!ifaceAWeak);
EXPECT_TRUE(!ifaceBWeak);
}
//
{
ASP<CChildA> childA = makeAtomicShared<CChildA>();
auto ifaceA = ASP<InterfaceA>(childA);
EXPECT_TRUE(ifaceA);
EXPECT_EQ(ifaceA->m_ifaceAInt, 69);
auto ifaceB = dynamicPointerCast<InterfaceA>(SP<CChildA>{});
auto ifaceB2 = dynamicPointerCast<InterfaceA>(WP<CChildA>{});
EXPECT_TRUE(!ifaceB);
EXPECT_TRUE(!ifaceB2);
}
{
ASP<CChild> child = makeAtomicShared<CChild>();
ASP<InterfaceA> ifaceA = dynamicPointerCast<InterfaceA>(child);
ASP<InterfaceB> ifaceB = dynamicPointerCast<InterfaceB>(child);
EXPECT_TRUE(ifaceA);
EXPECT_TRUE(ifaceB);
EXPECT_EQ(ifaceA->m_ifaceAInt, 69);
EXPECT_EQ(ifaceB->m_ifaceBInt, 2);
AWP<InterfaceA> ifaceAWeak = ifaceA;
AWP<InterfaceB> ifaceBWeak = dynamicPointerCast<InterfaceB>(AWP<CChild>{child});
child.reset();
EXPECT_TRUE(ifaceAWeak);
EXPECT_TRUE(ifaceBWeak);
EXPECT_TRUE(ifaceA);
EXPECT_EQ(ifaceAWeak->m_ifaceAInt, 69);
EXPECT_EQ(ifaceA->m_ifaceAInt, 69);
ifaceA.reset();
EXPECT_TRUE(ifaceAWeak);
EXPECT_EQ(ifaceAWeak->m_ifaceAInt, 69);
EXPECT_TRUE(ifaceB);
EXPECT_EQ(ifaceB->m_ifaceBInt, 2);
EXPECT_EQ(ifaceBWeak->m_ifaceBInt, 2);
ifaceB.reset();
EXPECT_TRUE(!ifaceAWeak);
EXPECT_TRUE(!ifaceBWeak);
}
// test for leaks
for (size_t i = 0; i < 10000; ++i) {
auto child = makeAtomicShared<CChild>();
auto child2 = makeShared<CChild>();
}
}
class CSelfDestruct {
public:
SP<CSelfDestruct> self;
//
void youShouldKysNOW() {
self.reset();
}
};
static void testSelfDestruct() {
auto x = makeShared<CSelfDestruct>();
x->self = x;
WP<CSelfDestruct> weak = x;
x.reset();
// this has no EXPECT, because all we check is if there isn't a UAF here.
// if there is, asan will abort us
weak->youShouldKysNOW();
}
TEST(Memory, memory) {
SP<int> intPtr = makeShared<int>(10);
SP<int> intPtr2 = makeShared<int>(-1337);
UP<int> intUnique = makeUnique<int>(420);
EXPECT_EQ(*intPtr, 10);
EXPECT_EQ(intPtr.strongRef(), 1);
EXPECT_EQ(*intUnique, 420);
WP<int> weak = intPtr;
WP<int> weakUnique = intUnique;
EXPECT_EQ(*intPtr, 10);
EXPECT_EQ(intPtr.strongRef(), 1);
EXPECT_EQ(*weak, 10);
EXPECT_EQ(weak.expired(), false);
EXPECT_EQ(!!weak.lock(), true);
EXPECT_EQ(*weakUnique, 420);
EXPECT_EQ(weakUnique.expired(), false);
EXPECT_EQ(intUnique.impl_->wref(), 1);
SP<int> sharedFromUnique = weakUnique.lock();
EXPECT_EQ(sharedFromUnique, nullptr);
std::vector<SP<int>> sps;
sps.push_back(intPtr);
sps.emplace_back(intPtr);
sps.push_back(intPtr2);
sps.emplace_back(intPtr2);
std::erase_if(sps, [intPtr](const auto& e) { return e == intPtr; });
intPtr.reset();
intUnique.reset();
EXPECT_EQ(weak.impl_->ref(), 0);
EXPECT_EQ(weakUnique.impl_->ref(), 0);
EXPECT_EQ(weakUnique.impl_->wref(), 1);
EXPECT_EQ(intPtr2.strongRef(), 3);
EXPECT_EQ(weak.expired(), true);
EXPECT_EQ(weakUnique.expired(), true);
auto intPtr2AsUint = reinterpretPointerCast<unsigned int>(intPtr2);
EXPECT_EQ(intPtr2.strongRef(), 4);
EXPECT_EQ(intPtr2AsUint.strongRef(), 4);
EXPECT_EQ(*intPtr2AsUint > 0, true);
EXPECT_EQ(*intPtr2AsUint, (unsigned int)(int)-1337);
*intPtr2AsUint = 10;
EXPECT_EQ(*intPtr2AsUint, 10);
EXPECT_EQ(*intPtr2, 10);
testAtomicImpl();
testHierarchy();
testSelfDestruct();
}

View file

@ -1,35 +0,0 @@
#include <csignal>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <hyprutils/os/Process.hpp>
#include "shared.hpp"
using namespace Hyprutils::OS;
int main(int argc, char** argv, char** envp) {
int ret = 0;
CProcess process("sh", {"-c", "echo \"Hello $WORLD!\""});
process.addEnv("WORLD", "World");
EXPECT(process.runAsync(), true);
EXPECT(process.runSync(), true);
EXPECT(process.stdOut(), std::string{"Hello World!\n"});
EXPECT(process.stdErr(), std::string{""});
EXPECT(process.exitCode(), 0);
CProcess process2("sh", {"-c", "while true; do sleep 1; done;"});
EXPECT(process2.runAsync(), true);
EXPECT(getpgid(process2.pid()) >= 0, true);
kill(process2.pid(), SIGKILL);
CProcess process3("sh", {"-c", "cat /geryueruggbuergheruger/reugiheruygyuerghuryeghyer/eruihgyuerguyerghyuerghuyergerguyer/NON_EXISTENT"});
EXPECT(process3.runSync(), true);
EXPECT(process3.exitCode(), 1);
return ret;
}

45
tests/os/Fd.cpp Normal file
View file

@ -0,0 +1,45 @@
#include <hyprutils/os/FileDescriptor.hpp>
#include <gtest/gtest.h>
#include <sys/mman.h>
using namespace Hyprutils::OS;
TEST(OS, fd) {
std::string name = "/test_filedescriptors";
CFileDescriptor fd(shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600));
EXPECT_EQ(fd.isValid(), true);
EXPECT_EQ(fd.isReadable(), true);
int flags = fd.getFlags();
EXPECT_EQ(fd.getFlags(), FD_CLOEXEC);
flags &= ~FD_CLOEXEC;
fd.setFlags(flags);
EXPECT_EQ(fd.getFlags(), !FD_CLOEXEC);
CFileDescriptor fd2 = fd.duplicate();
EXPECT_EQ(fd.isValid(), true);
EXPECT_EQ(fd.isReadable(), true);
EXPECT_EQ(fd2.isValid(), true);
EXPECT_EQ(fd2.isReadable(), true);
CFileDescriptor fd3(fd2.take());
EXPECT_EQ(fd.isValid(), true);
EXPECT_EQ(fd.isReadable(), true);
EXPECT_EQ(fd2.isValid(), false);
EXPECT_EQ(fd2.isReadable(), false);
// .duplicate default flags is FD_CLOEXEC
EXPECT_EQ(fd3.getFlags(), FD_CLOEXEC);
fd.reset();
fd2.reset();
fd3.reset();
EXPECT_EQ(fd.isReadable(), false);
EXPECT_EQ(fd2.isReadable(), false);
EXPECT_EQ(fd3.isReadable(), false);
shm_unlink(name.c_str());
}

31
tests/os/Process.cpp Normal file
View file

@ -0,0 +1,31 @@
#include <hyprutils/os/Process.hpp>
#include <gtest/gtest.h>
#include <sys/types.h>
#include <signal.h>
using namespace Hyprutils::OS;
TEST(OS, process) {
CProcess process("sh", {"-c", "echo \"Hello $WORLD!\""});
process.addEnv("WORLD", "World");
EXPECT_EQ(process.runAsync(), true);
EXPECT_EQ(process.runSync(), true);
EXPECT_EQ(process.stdOut(), std::string{"Hello World!\n"});
EXPECT_EQ(process.stdErr(), std::string{""});
EXPECT_EQ(process.exitCode(), 0);
CProcess process2("sh", {"-c", "while true; do sleep 1; done;"});
EXPECT_EQ(process2.runAsync(), true);
EXPECT_EQ(getpgid(process2.pid()) >= 0, true);
kill(process2.pid(), SIGKILL);
CProcess process3("sh", {"-c", "cat /geryueruggbuergheruger/reugiheruygyuerghuryeghyer/eruihgyuerguyerghyuerghuyergerguyer/NON_EXISTENT"});
EXPECT_EQ(process3.runSync(), true);
EXPECT_EQ(process3.exitCode(), 1);
}

60
tests/path/Path.cpp Normal file
View file

@ -0,0 +1,60 @@
#include <hyprutils/path/Path.hpp>
#include <filesystem>
#include <gtest/gtest.h>
using namespace Hyprutils::Path;
static std::string cwd() {
return std::filesystem::current_path().string();
}
static std::string home() {
const char* h = getenv("HOME");
return h ? std::string(h) : std::string{};
}
TEST(Path, resolvePath_absolute) {
EXPECT_EQ(resolvePath("/foo/bar/baz").value(), "/foo/bar/baz");
EXPECT_EQ(resolvePath("/foo/./bar/../baz").value(), "/foo/baz");
EXPECT_EQ(resolvePath("/a/b/../../c").value(), "/c");
EXPECT_EQ(resolvePath("/a/b/c/./././../d").value(), "/a/b/d");
}
TEST(Path, resolvePath_dot) {
const std::string base = "/some/base";
EXPECT_EQ(resolvePath("./here", base).value(), "/some/base/here");
EXPECT_EQ(resolvePath("./a/b/c", base).value(), "/some/base/a/b/c");
EXPECT_EQ(resolvePath("./here").value(), cwd() + "/here");
}
TEST(Path, resolvePath_dotdot) {
const std::string base = "/some/base/dir";
EXPECT_EQ(resolvePath("../here", base).value(), "/some/base/here");
EXPECT_EQ(resolvePath("../../here", base).value(), "/some/here");
EXPECT_EQ(resolvePath("../../../../././.here", base).value(), "/.here");
EXPECT_EQ(resolvePath("./a/../../b/../c", base).value(), "/some/base/c");
}
TEST(Path, resolvePath_tilde) {
const std::string h = home();
if (h.empty())
GTEST_SKIP() << "HOME not set, skipping ~ tests";
EXPECT_EQ(resolvePath("~").value(), h);
EXPECT_EQ(resolvePath("~/").value(), h + "/");
EXPECT_EQ(resolvePath("~/foo/bar").value(), h + "/foo/bar");
EXPECT_EQ(resolvePath("~/foo/../bar").value(), h + "/bar");
EXPECT_EQ(resolvePath("~/./foo/./bar").value(), h + "/foo/bar");
}
TEST(Path, resolvePath_relative_base) {
EXPECT_EQ(resolvePath("file.txt", "subdir").value(), cwd() + "/subdir/file.txt");
EXPECT_EQ(resolvePath("../file.txt", "subdir").value(), cwd() + "/file.txt");
}
TEST(Path, resolvePath_errors) {
EXPECT_FALSE(resolvePath("").has_value());
EXPECT_FALSE(resolvePath("~nohome").has_value());
}

View file

@ -1,32 +0,0 @@
#pragma once
#include <iostream>
namespace Colors {
constexpr const char* RED = "\x1b[31m";
constexpr const char* GREEN = "\x1b[32m";
constexpr const char* YELLOW = "\x1b[33m";
constexpr const char* BLUE = "\x1b[34m";
constexpr const char* MAGENTA = "\x1b[35m";
constexpr const char* CYAN = "\x1b[36m";
constexpr const char* RESET = "\x1b[0m";
};
#define EXPECT(expr, val) \
if (const auto RESULT = expr; RESULT != (val)) { \
std::cout << Colors::RED << "Failed: " << Colors::RESET << #expr << ", expected " << val << " but got " << RESULT << "\n"; \
ret = 1; \
} else { \
std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got " << val << "\n"; \
}
#define EXPECT_VECTOR2D(expr, val) \
do { \
const auto& RESULT = expr; \
const auto& EXPECTED = val; \
if (!(std::abs(RESULT.x - EXPECTED.x) < 1e-6 && std::abs(RESULT.y - EXPECTED.y) < 1e-6)) { \
std::cout << Colors::RED << "Failed: " << Colors::RESET << #expr << ", expected (" << EXPECTED.x << ", " << EXPECTED.y << ") but got (" << RESULT.x << ", " \
<< RESULT.y << ")\n"; \
ret = 1; \
} else { \
std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got (" << RESULT.x << ", " << RESULT.y << ")\n"; \
} \
} while (0)

View file

@ -1,10 +1,11 @@
#include <gtest/gtest.h>
#include <any> #include <any>
#include <hyprutils/signal/Signal.hpp> #include <hyprutils/signal/Signal.hpp>
#include <hyprutils/signal/Listener.hpp>
#include <hyprutils/memory/WeakPtr.hpp> #include <hyprutils/memory/WeakPtr.hpp>
#include <memory> #include <memory>
#include "hyprutils/memory/SharedPtr.hpp"
#include "hyprutils/signal/Listener.hpp"
#include "shared.hpp"
using namespace Hyprutils::Signal; using namespace Hyprutils::Signal;
using namespace Hyprutils::Memory; using namespace Hyprutils::Memory;
@ -13,14 +14,14 @@ using namespace Hyprutils::Memory;
#pragma GCC diagnostic ignored "-Wdeprecated-declarations" #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// //
void legacy(int& ret) { static void legacy() {
CSignal signal; CSignal signal;
int data = 0; int data = 0;
auto listener = signal.registerListener([&]([[maybe_unused]] std::any d) { data = 1; }); auto listener = signal.registerListener([&]([[maybe_unused]] std::any d) { data = 1; });
signal.emit(); signal.emit();
EXPECT(data, 1); EXPECT_EQ(data, 1);
data = 0; data = 0;
@ -28,19 +29,19 @@ void legacy(int& ret) {
signal.emit(); signal.emit();
EXPECT(data, 0); EXPECT_EQ(data, 0);
} }
void legacyListenerEmit(int& ret) { static void legacyListenerEmit() {
int data = 0; int data = 0;
CSignal signal; CSignal signal;
auto listener = signal.registerListener([&](std::any d) { data = std::any_cast<int>(d); }); auto listener = signal.registerListener([&](std::any d) { data = std::any_cast<int>(d); });
listener->emit(1); // not a typo listener->emit(1); // not a typo
EXPECT(data, 1); EXPECT_EQ(data, 1);
} }
void legacyListeners(int& ret) { static void legacyListeners() {
int data = 0; int data = 0;
CSignalT<> signal0; CSignalT<> signal0;
@ -55,38 +56,38 @@ void legacyListeners(int& ret) {
signal0.emit(); signal0.emit();
signal1.emit(2); signal1.emit(2);
EXPECT(data, 33); EXPECT_EQ(data, 33);
} }
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
// //
void empty(int& ret) { static void empty() {
int data = 0; int data = 0;
CSignalT<> signal; CSignalT<> signal;
auto listener = signal.listen([&] { data = 1; }); auto listener = signal.listen([&] { data = 1; });
signal.emit(); signal.emit();
EXPECT(data, 1); EXPECT_EQ(data, 1);
data = 0; data = 0;
listener.reset(); listener.reset();
signal.emit(); signal.emit();
EXPECT(data, 0); EXPECT_EQ(data, 0);
} }
void typed(int& ret) { static void typed() {
int data = 0; int data = 0;
CSignalT<int> signal; CSignalT<int> signal;
auto listener = signal.listen([&](int newData) { data = newData; }); auto listener = signal.listen([&](int newData) { data = newData; });
signal.emit(1); signal.emit(1);
EXPECT(data, 1); EXPECT_EQ(data, 1);
} }
void ignoreParams(int& ret) { static void ignoreParams() {
int data = 0; int data = 0;
CSignalT<int> signal; CSignalT<int> signal;
@ -95,10 +96,10 @@ void ignoreParams(int& ret) {
signal.listenStatic([&] { data += 1; }); signal.listenStatic([&] { data += 1; });
signal.emit(2); signal.emit(2);
EXPECT(data, 2); EXPECT_EQ(data, 2);
} }
void typedMany(int& ret) { static void typedMany() {
int data1 = 0; int data1 = 0;
int data2 = 0; int data2 = 0;
int data3 = 0; int data3 = 0;
@ -111,12 +112,12 @@ void typedMany(int& ret) {
}); });
signal.emit(1, 2, 3); signal.emit(1, 2, 3);
EXPECT(data1, 1); EXPECT_EQ(data1, 1);
EXPECT(data2, 2); EXPECT_EQ(data2, 2);
EXPECT(data3, 3); EXPECT_EQ(data3, 3);
} }
void ref(int& ret) { static void ref() {
int count = 0; int count = 0;
int data = 0; int data = 0;
@ -130,11 +131,11 @@ void ref(int& ret) {
auto l4 = constSignal.listen([&](int v) { count += v; }); auto l4 = constSignal.listen([&](int v) { count += v; });
constSignal.emit(data); constSignal.emit(data);
EXPECT(data, 1); EXPECT_EQ(data, 1);
EXPECT(count, 3); EXPECT_EQ(count, 3);
} }
void refMany(int& ret) { static void refMany() {
int count = 0; int count = 0;
int data1 = 0; int data1 = 0;
int data2 = 10; int data2 = 10;
@ -144,11 +145,11 @@ void refMany(int& ret) {
auto l2 = signal.listen([&](int v1, int v2) { count += v1 + v2; }); auto l2 = signal.listen([&](int v1, int v2) { count += v1 + v2; });
signal.emit(data1, data2); signal.emit(data1, data2);
EXPECT(data1, 1); EXPECT_EQ(data1, 1);
EXPECT(count, 11); EXPECT_EQ(count, 11);
} }
void autoRefTypes(int& ret) { static void autoRefTypes() {
class CCopyCounter { class CCopyCounter {
public: public:
CCopyCounter(int& createCount, int& destroyCount) : createCount(createCount), destroyCount(destroyCount) { CCopyCounter(int& createCount, int& destroyCount) : createCount(createCount), destroyCount(destroyCount) {
@ -174,11 +175,11 @@ void autoRefTypes(int& ret) {
auto listener = signal.listen([](const CCopyCounter& counter) {}); auto listener = signal.listen([](const CCopyCounter& counter) {});
signal.emit(CCopyCounter(createCount, destroyCount)); signal.emit(CCopyCounter(createCount, destroyCount));
EXPECT(createCount, 1); EXPECT_EQ(createCount, 1);
EXPECT(destroyCount, 1); EXPECT_EQ(destroyCount, 1);
} }
void forward(int& ret) { static void forward() {
int count = 0; int count = 0;
CSignalT<int> sig; CSignalT<int> sig;
@ -193,10 +194,10 @@ void forward(int& ret) {
sig.emit(2); sig.emit(2);
EXPECT(count, 3); EXPECT_EQ(count, 3);
} }
void listenerAdded(int& ret) { static void listenerAdded() {
int count = 0; int count = 0;
CSignalT<> signal; CSignalT<> signal;
@ -210,13 +211,13 @@ void listenerAdded(int& ret) {
}); });
signal.emit(); signal.emit();
EXPECT(count, 1); // second should NOT be invoked as it was registed during emit EXPECT_EQ(count, 1); // second should NOT be invoked as it was registed during emit
signal.emit(); signal.emit();
EXPECT(count, 3); // second should be invoked EXPECT_EQ(count, 3); // second should be invoked
} }
void lastListenerSwapped(int& ret) { static void lastListenerSwapped() {
int count = 0; int count = 0;
CSignalT<> signal; CSignalT<> signal;
@ -233,13 +234,13 @@ void lastListenerSwapped(int& ret) {
removedListener = signal.listen([&] { count += 1; }); removedListener = signal.listen([&] { count += 1; });
signal.emit(); signal.emit();
EXPECT(count, 0); // neither the removed nor added listeners should fire EXPECT_EQ(count, 0); // neither the removed nor added listeners should fire
signal.emit(); signal.emit();
EXPECT(count, 2); // only the new listener should fire EXPECT_EQ(count, 2); // only the new listener should fire
} }
void signalDestroyed(int& ret) { static void signalDestroyed() {
int count = 0; int count = 0;
auto signal = std::make_unique<CSignalT<>>(); auto signal = std::make_unique<CSignalT<>>();
@ -254,11 +255,11 @@ void signalDestroyed(int& ret) {
auto postListener = signal->listen([&] { count += 1; }); auto postListener = signal->listen([&] { count += 1; });
signal->emit(); signal->emit();
EXPECT(count, 2); // all listeners should fire regardless of signal deletion EXPECT_EQ(count, 2); // all listeners should fire regardless of signal deletion
} }
// purely an asan test // purely an asan test
void signalDestroyedBeforeListener() { static void signalDestroyedBeforeListener() {
CHyprSignalListener listener1; CHyprSignalListener listener1;
CHyprSignalListener listener2; CHyprSignalListener listener2;
@ -268,7 +269,7 @@ void signalDestroyedBeforeListener() {
listener2 = signal.listen([] {}); listener2 = signal.listen([] {});
} }
void signalDestroyedWithAddedListener(int& ret) { static void signalDestroyedWithAddedListener() {
int count = 0; int count = 0;
auto signal = std::make_unique<CSignalT<>>(); auto signal = std::make_unique<CSignalT<>>();
@ -280,10 +281,10 @@ void signalDestroyedWithAddedListener(int& ret) {
}); });
signal->emit(); signal->emit();
EXPECT(count, 0); EXPECT_EQ(count, 0);
} }
void signalDestroyedWithRemovedAndAddedListener(int& ret) { static void signalDestroyedWithRemovedAndAddedListener() {
int count = 0; int count = 0;
auto signal = std::make_unique<CSignalT<>>(); auto signal = std::make_unique<CSignalT<>>();
@ -299,20 +300,20 @@ void signalDestroyedWithRemovedAndAddedListener(int& ret) {
removed = signal->listen([&] { count += 1; }); removed = signal->listen([&] { count += 1; });
signal->emit(); signal->emit();
EXPECT(count, 0); EXPECT_EQ(count, 0);
} }
void staticListener(int& ret) { static void staticListener() {
int data = 0; int data = 0;
CSignalT<int> signal; CSignalT<int> signal;
signal.listenStatic([&](int newData) { data = newData; }); signal.listenStatic([&](int newData) { data = newData; });
signal.emit(1); signal.emit(1);
EXPECT(data, 1); EXPECT_EQ(data, 1);
} }
void staticListenerDestroy(int& ret) { static void staticListenerDestroy() {
int count = 0; int count = 0;
auto signal = makeShared<CSignalT<>>(); auto signal = makeShared<CSignalT<>>();
@ -328,11 +329,11 @@ void staticListenerDestroy(int& ret) {
signal->listenStatic([&] { count += 1; }); signal->listenStatic([&] { count += 1; });
signal->emit(); signal->emit();
EXPECT(count, 2); EXPECT_EQ(count, 2);
} }
// purely an asan test // purely an asan test
void listenerDestroysSelf() { static void listenerDestroysSelf() {
CSignalT<> signal; CSignalT<> signal;
CHyprSignalListener listener; CHyprSignalListener listener;
@ -343,28 +344,26 @@ void listenerDestroysSelf() {
signal.emit(); signal.emit();
} }
int main(int argc, char** argv, char** envp) { TEST(Signal, signal) {
int ret = 0; legacy();
legacy(ret); legacyListenerEmit();
legacyListenerEmit(ret); legacyListeners();
legacyListeners(ret); empty();
empty(ret); typed();
typed(ret); ignoreParams();
ignoreParams(ret); typedMany();
typedMany(ret); ref();
ref(ret); refMany();
refMany(ret); autoRefTypes();
autoRefTypes(ret); forward();
forward(ret); listenerAdded();
listenerAdded(ret); lastListenerSwapped();
lastListenerSwapped(ret); signalDestroyed();
signalDestroyed(ret);
signalDestroyedBeforeListener(); signalDestroyedBeforeListener();
signalDestroyedWithAddedListener(ret); signalDestroyedWithAddedListener();
signalDestroyedWithRemovedAndAddedListener(ret); signalDestroyedWithRemovedAndAddedListener();
staticListener(ret); staticListener();
staticListenerDestroy(ret); staticListenerDestroy();
signalDestroyed(ret); signalDestroyed();
listenerDestroysSelf(); listenerDestroysSelf();
return ret;
} }

View file

@ -1,51 +0,0 @@
#include <hyprutils/string/String.hpp>
#include <hyprutils/string/VarList.hpp>
#include <hyprutils/string/ConstVarList.hpp>
#include "shared.hpp"
using namespace Hyprutils::String;
int main(int argc, char** argv, char** envp) {
int ret = 0;
EXPECT(trim(" a "), "a");
EXPECT(trim(" a a "), "a a");
EXPECT(trim("a"), "a");
EXPECT(trim(" "), "");
EXPECT(isNumber("99214123434"), true);
EXPECT(isNumber("-35252345234"), true);
EXPECT(isNumber("---3423--432"), false);
EXPECT(isNumber("s---3423--432"), false);
EXPECT(isNumber("---3423--432s"), false);
EXPECT(isNumber("1s"), false);
EXPECT(isNumber(""), false);
EXPECT(isNumber("-"), false);
EXPECT(isNumber("--0"), false);
EXPECT(isNumber("abc"), false);
EXPECT(isNumber("0.0", true), true);
EXPECT(isNumber("0.2", true), true);
EXPECT(isNumber("0.", true), false);
EXPECT(isNumber(".0", true), false);
EXPECT(isNumber("", true), false);
EXPECT(isNumber("vvss", true), false);
EXPECT(isNumber("0.9999s", true), false);
EXPECT(isNumber("s0.9999", true), false);
EXPECT(isNumber("-1.0", true), true);
EXPECT(isNumber("-1..0", true), false);
EXPECT(isNumber("-10.0000000001", true), true);
CVarList list("hello world!", 0, 's', true);
EXPECT(list[0], "hello");
EXPECT(list[1], "world!");
CConstVarList listConst("hello world!", 0, 's', true);
EXPECT(listConst[0], "hello");
EXPECT(listConst[1], "world!");
std::string hello = "hello world!";
replaceInString(hello, "hello", "hi");
EXPECT(hello, "hi world!");
return ret;
}

View file

@ -0,0 +1,15 @@
#include <hyprutils/string/ConstVarList.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::String;
TEST(String, constvarlist) {
CConstVarList listConst("hello world!", 0, 's', true);
EXPECT_EQ(listConst[0], "hello");
EXPECT_EQ(listConst[1], "world!");
CConstVarList listConst2("0 set", 2, ' ');
EXPECT_EQ(listConst2[0], "0");
EXPECT_EQ(listConst2[1], "set");
}

143
tests/string/Numeric.cpp Normal file
View file

@ -0,0 +1,143 @@
#include <hyprutils/string/Numeric.hpp>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
using namespace Hyprutils::String;
TEST(Numeric, intSuccess) {
auto r = strToNumber<int>("42");
ASSERT_TRUE(r.has_value());
EXPECT_EQ(*r, 42);
auto neg = strToNumber<int>("-7");
ASSERT_TRUE(neg.has_value());
EXPECT_EQ(*neg, -7);
auto zero = strToNumber<int>("0");
ASSERT_TRUE(zero.has_value());
EXPECT_EQ(*zero, 0);
}
TEST(Numeric, intBounds) {
const auto maxStr = std::to_string(std::numeric_limits<int32_t>::max());
auto rMax = strToNumber<int32_t>(maxStr);
ASSERT_TRUE(rMax.has_value());
EXPECT_EQ(*rMax, std::numeric_limits<int32_t>::max());
const auto minStr = std::to_string(std::numeric_limits<int32_t>::min());
auto rMin = strToNumber<int32_t>(minStr);
ASSERT_TRUE(rMin.has_value());
EXPECT_EQ(*rMin, std::numeric_limits<int32_t>::min());
}
TEST(Numeric, outOfRange) {
auto r = strToNumber<int8_t>("999");
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), NUMERIC_PARSE_OUT_OF_RANGE);
// from_chars rejects '-' for unsigned types as invalid argument, not out-of-range
auto r2 = strToNumber<uint8_t>("-1");
ASSERT_FALSE(r2.has_value());
EXPECT_EQ(r2.error(), NUMERIC_PARSE_BAD);
}
TEST(Numeric, garbage) {
auto r = strToNumber<int>("12abc");
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), NUMERIC_PARSE_GARBAGE);
auto r2 = strToNumber<double>("1.0xyz");
ASSERT_FALSE(r2.has_value());
EXPECT_EQ(r2.error(), NUMERIC_PARSE_GARBAGE);
}
TEST(Numeric, bad) {
auto r = strToNumber<int>("");
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), NUMERIC_PARSE_BAD);
auto r2 = strToNumber<int>("abc");
ASSERT_FALSE(r2.has_value());
EXPECT_EQ(r2.error(), NUMERIC_PARSE_BAD);
auto r3 = strToNumber<int>("--1");
ASSERT_FALSE(r3.has_value());
EXPECT_EQ(r3.error(), NUMERIC_PARSE_BAD);
}
TEST(Numeric, floatSuccess) {
auto r = strToNumber<double>("3.14");
ASSERT_TRUE(r.has_value());
EXPECT_DOUBLE_EQ(*r, 3.14);
auto neg = strToNumber<float>("-0.5");
ASSERT_TRUE(neg.has_value());
EXPECT_FLOAT_EQ(*neg, -0.5f);
auto whole = strToNumber<double>("100");
ASSERT_TRUE(whole.has_value());
EXPECT_DOUBLE_EQ(*whole, 100.0);
}
TEST(Numeric, unsignedTypes) {
auto r = strToNumber<uint64_t>("18446744073709551615");
ASSERT_TRUE(r.has_value());
EXPECT_EQ(*r, std::numeric_limits<uint64_t>::max());
auto r2 = strToNumber<uint32_t>("0");
ASSERT_TRUE(r2.has_value());
EXPECT_EQ(*r2, 0u);
}
TEST(Numeric, hexSuccess) {
auto r = strToNumber<int>("0xAF23");
ASSERT_TRUE(r.has_value());
EXPECT_EQ(*r, 0xAF23);
auto r2 = strToNumber<uint32_t>("0xFF");
ASSERT_TRUE(r2.has_value());
EXPECT_EQ(*r2, 0xFFu);
auto r3 = strToNumber<uint64_t>("0xDEADBEEF");
ASSERT_TRUE(r3.has_value());
EXPECT_EQ(*r3, 0xDEADBEEFu);
// uppercase X prefix
auto r4 = strToNumber<int>("0XFF");
ASSERT_TRUE(r4.has_value());
EXPECT_EQ(*r4, 0xFF);
// lowercase hex digits
auto r5 = strToNumber<uint32_t>("0xdeadbeef");
ASSERT_TRUE(r5.has_value());
EXPECT_EQ(*r5, 0xDEADBEEFu);
// zero value
auto r6 = strToNumber<int>("0x0");
ASSERT_TRUE(r6.has_value());
EXPECT_EQ(*r6, 0);
}
TEST(Numeric, hexErrors) {
// incomplete prefix (just "0x")
auto r = strToNumber<int>("0x");
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), NUMERIC_PARSE_BAD);
// garbage after valid hex digits
auto r2 = strToNumber<int>("0xFF_ZZ");
ASSERT_FALSE(r2.has_value());
EXPECT_EQ(r2.error(), NUMERIC_PARSE_GARBAGE);
// out of range for type
auto r3 = strToNumber<uint8_t>("0xFFF");
ASSERT_FALSE(r3.has_value());
EXPECT_EQ(r3.error(), NUMERIC_PARSE_OUT_OF_RANGE);
// floats do not accept hex prefix
auto r4 = strToNumber<double>("0xFF");
ASSERT_FALSE(r4.has_value());
EXPECT_EQ(r4.error(), NUMERIC_PARSE_GARBAGE);
}

47
tests/string/String.cpp Normal file
View file

@ -0,0 +1,47 @@
#include <hyprutils/string/String.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::String;
TEST(String, string) {
EXPECT_EQ(trim(" a "), "a");
EXPECT_EQ(trim(" a a "), "a a");
EXPECT_EQ(trim("a"), "a");
EXPECT_EQ(trim(" "), "");
EXPECT_EQ(isNumber("99214123434"), true);
EXPECT_EQ(isNumber("-35252345234"), true);
EXPECT_EQ(isNumber("---3423--432"), false);
EXPECT_EQ(isNumber("s---3423--432"), false);
EXPECT_EQ(isNumber("---3423--432s"), false);
EXPECT_EQ(isNumber("1s"), false);
EXPECT_EQ(isNumber(""), false);
EXPECT_EQ(isNumber("-"), false);
EXPECT_EQ(isNumber("--0"), false);
EXPECT_EQ(isNumber("abc"), false);
EXPECT_EQ(isNumber("0.0", true), true);
EXPECT_EQ(isNumber("0.2", true), true);
EXPECT_EQ(isNumber("0.", true), false);
EXPECT_EQ(isNumber(".0", true), false);
EXPECT_EQ(isNumber("", true), false);
EXPECT_EQ(isNumber("vvss", true), false);
EXPECT_EQ(isNumber("0.9999s", true), false);
EXPECT_EQ(isNumber("s0.9999", true), false);
EXPECT_EQ(isNumber("-1.0", true), true);
EXPECT_EQ(isNumber("-1..0", true), false);
EXPECT_EQ(isNumber("-10.0000000001", true), true);
EXPECT_EQ(truthy("frgeujgeruibger"), false);
EXPECT_EQ(truthy("false"), false);
EXPECT_EQ(truthy("0"), false);
EXPECT_EQ(truthy("yees"), false);
EXPECT_EQ(truthy("naa"), false);
EXPECT_EQ(truthy("-1"), false);
EXPECT_EQ(truthy("true"), true);
EXPECT_EQ(truthy("true eeee ee"), true);
EXPECT_EQ(truthy("yesss"), true);
EXPECT_EQ(truthy("1"), true);
EXPECT_EQ(truthy("on"), true);
EXPECT_EQ(truthy("onn"), true);
}

11
tests/string/VarList.cpp Normal file
View file

@ -0,0 +1,11 @@
#include <hyprutils/string/VarList.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::String;
TEST(String, varlist) {
CVarList list("hello world!", 0, 's', true);
EXPECT_EQ(list[0], "hello");
EXPECT_EQ(list[1], "world!");
}

67
tests/string/VarList2.cpp Normal file
View file

@ -0,0 +1,67 @@
#include <hyprutils/string/VarList2.hpp>
#include <gtest/gtest.h>
using namespace Hyprutils::String;
TEST(String, varlist2) {
CVarList2 varList2("0 set", 2, ' ');
EXPECT_EQ(varList2[0], "0");
EXPECT_EQ(varList2[1], "set");
varList2.append("Hello");
EXPECT_EQ(varList2[1], "set");
EXPECT_EQ(varList2[2], "Hello");
EXPECT_EQ(varList2[3], "");
EXPECT_EQ(varList2.contains("set"), true);
EXPECT_EQ(varList2.contains("sett"), false);
EXPECT_EQ(varList2.contains(""), false);
EXPECT_EQ(varList2.size(), 3);
CVarList2 varList2B("hello, world\\, ok?", 0, ',', true, true);
EXPECT_EQ(varList2B[0], "hello");
EXPECT_EQ(varList2B[1], "world, ok?");
CVarList2 varList2C("hello, , ok?", 0, ',', true, true);
EXPECT_EQ(varList2C[0], "hello");
EXPECT_EQ(varList2C[1], "ok?");
CVarList2 varList2D("\\\\, , ok?", 0, ',', true, true);
EXPECT_EQ(varList2D[0], "\\");
EXPECT_EQ(varList2D[1], "ok?");
CVarList2 varList2E("\\, , ok?", 0, ',', true, true);
EXPECT_EQ(varList2E[0], ",");
EXPECT_EQ(varList2E[1], "ok?");
CVarList2 varList2F("Hello, world\\\\, ok?", 0, ',', true, true);
EXPECT_EQ(varList2F[0], "Hello");
EXPECT_EQ(varList2F[1], "world\\");
EXPECT_EQ(varList2F[2], "ok?");
CVarList2 varList2G("Hello,\\, ok?", 0, ',', true, true);
EXPECT_EQ(varList2G[0], "Hello");
EXPECT_EQ(varList2G[1], ", ok?");
CVarList2 varList2H("Hello,\\\\, ok?", 0, ',', true, true);
EXPECT_EQ(varList2H[0], "Hello");
EXPECT_EQ(varList2H[1], "\\");
EXPECT_EQ(varList2H[2], "ok?");
CVarList2 varList2I("Hello,\\, ok?", 0, ',', true, false);
EXPECT_EQ(varList2I[0], "Hello");
EXPECT_EQ(varList2I[1], "\\");
EXPECT_EQ(varList2I[2], "ok?");
CVarList2 varList2J("", 0, ',', true, false);
EXPECT_EQ(varList2J[0], "");
CVarList2 varList2K(",\\, ok?", 0, ',', true);
EXPECT_EQ(varList2K[0], ", ok?");
CVarList2 varList2L("Hello, world", 0, ',', true);
EXPECT_EQ(varList2L.join(" "), "Hello world");
EXPECT_EQ(varList2L.join(" ", 0, 1000), "Hello world");
EXPECT_EQ(varList2L.join(" ", 0, 1), "Hello");
}