Compare commits

...

39 commits
v0.5.3 ... main

Author SHA1 Message Date
0d00dc1189
version: bump to 0.6.7 2025-12-01 18:07:10 +00:00
EvilLary
eb5be96aa0
core: fix crash with same name special category and keyword (#87)
---------

Co-authored-by: Vaxry <vaxry@vaxry.net>
2025-11-28 16:52:41 +00:00
3d66ec7c29
core: add changeRootPath for CConfig 2025-11-27 15:46:35 +00:00
a318deec0c
core: fix dynamic env changes 2025-11-22 13:54:21 +00:00
3d3057837c
version: bump to 0.6.6 2025-11-17 18:36:20 +00:00
deea98d5b6
config/parser: don't return found on dontErrorOnMissing in special
fixes https://github.com/hyprwm/hyprland-plugins/issues/539
2025-11-16 00:51:32 +00:00
8b3da759ec
version: bump to 0.6.5 2025-11-14 19:34:08 +00:00
b0d7b375a9
parser: fix lingering currentSpecialCat after dynamic calls 2025-11-13 17:42:52 +00:00
4302343ce7
ci/arch: add gtest 2025-11-13 15:59:41 +00:00
771e915f59
config/parser: fix invalid ptr after move 2025-11-12 13:51:28 +00:00
995db114b8
config: try variables before handlers if possible 2025-11-11 21:36:52 +00:00
Maximilian Seidler
4dafa28d4f
core: support nesting with special categories and fix explicit key + nested (#82) 2025-09-27 01:05:02 +02:00
3d63fb4a42
config: allow nesting if statements 2025-09-02 12:51:41 +02:00
crispy-caesus
23f0debd20
README: update docs link (#81) 2025-07-27 16:28:12 +03:00
12cb0e19e3
tests: minor typo fix 2025-07-25 21:26:39 +02:00
13865735fe
core: fix negating of ifs 2025-07-25 21:24:08 +02:00
235ce61cba
core: add support for conditional statements
Adds support for simple if / endif statements operating on variables

Fixes #52
2025-07-25 21:20:34 +02:00
cee01452bc
CI/Nix: add cache-nix-action
Use nixbuild/nix-quick-install-action which pairs well with
nix-community/cache-nix-action.

Should help with build times by reducing the number of packages needing
to be re-downloaded on each run.

Parameters are taken from https://github.com/nix-community/cache-nix-action
and may be tweaked later.
2025-06-20 01:13:18 +03:00
Friday
1bfb84f54d
nix: use gcc15 (#79)
also updated dependencies
2025-06-05 18:51:22 +01:00
Jonathan Steininger
163c83b3db
parser: Add ability to escape {{EXPRESSION}} syntax from #75 (#76) 2025-05-17 14:29:35 +02:00
557241780c
version: bump to 0.6.3 2025-05-07 23:03:32 +01:00
a59e86a3da
parser: change expression syntax to avoid bash clashes
changes from $() to {{}} to avoid clashing with bash syntax
2025-05-07 23:03:03 +01:00
a15e7ba78a
version: bump to 0.6.2 2025-05-07 19:46:08 +01:00
e863ebcee9
flake.lock: update 2025-05-07 20:11:54 +03:00
6726cfd54b
parser: add support for basic arithmetic
Adds support for  expressions that take left and right hand side and an operation (+-*/) -> e.g.

fixes #67
2025-05-07 17:50:22 +01:00
Honkazel
397600c42b
clang-tidy: fix some errors (#70) 2025-04-22 23:23:39 +02:00
f1000c54d2 version: bump to 0.6.1 2025-04-12 15:35:25 +01:00
Lukas
72df3861f1
README: fix typo (#65) 2025-03-05 18:18:47 +02:00
1d7d96a278 docs: fix incorrect doc about getValue type 2025-02-25 17:18:11 +00:00
a7334904d5
CI: remove deprecated magic-nix-cache-action 2025-02-08 23:08:34 +02:00
Honkazel
7a59f2de3f
core: clang-tidy and comp fixes (#63) 2025-02-08 01:44:54 +01:00
f41271d35c
flake.lock: update 2025-01-23 14:16:46 +02:00
Joshua Baker
55608efdaa
core: add multiline support (#58)
Adds support for multi-line commands with a backslash
2024-12-28 13:36:59 +00:00
0404833ea1
flake.lock: update 2024-12-23 00:24:19 +02:00
16e59c1eb1
flake.lock: update 2024-12-16 17:57:08 +02:00
9441266c89 cmakelists: remove clang-specific config
closes #59
2024-12-14 23:34:04 +00:00
f7acd5dabb
flake.nix: use gcc14Stdenv, update 2024-12-14 13:01:16 +02:00
1b0c595731 version: bump to 0.6.0 2024-12-13 20:48:31 +00:00
058fcf84c9 API: add a templated config value wrapper
This is basically a copy of the Hyprland config wrapper. It's much more straightforward to use, and much harder to fuck up.
2024-12-13 20:48:06 +00:00
17 changed files with 1015 additions and 281 deletions

101
.clang-tidy Normal file
View file

@ -0,0 +1,101 @@
WarningsAsErrors: '*'
HeaderFilterRegex: '.*\.hpp'
FormatStyle: 'file'
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declaration-namespace,
-bugprone-forward-declaration-namespace,
-bugprone-macro-parentheses,
-bugprone-narrowing-conversions,
-bugprone-branch-clone,
-bugprone-assignment-in-if-condition,
concurrency-*,
-concurrency-mt-unsafe,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-explicit-virtual-functions,
-cppcoreguidelines-avoid-c-arrays,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-narrowing-conversions,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-reinterpret-cast,
google-global-names-in-headers,
-google-readability-casting,
google-runtime-operator,
misc-*,
-misc-unused-parameters,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-include-cleaner,
-misc-use-anonymous-namespace,
-misc-const-correctness,
modernize-*,
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
-modernize-use-using,
-modernize-use-override,
-modernize-avoid-c-arrays,
-modernize-macro-to-enum,
-modernize-loop-convert,
-modernize-use-nodiscard,
-modernize-pass-by-value,
-modernize-use-auto,
performance-*,
-performance-avoid-endl,
-performance-unnecessary-value-param,
portability-std-allocator-const,
readability-*,
-readability-function-cognitive-complexity,
-readability-function-size,
-readability-identifier-length,
-readability-magic-numbers,
-readability-uppercase-literal-suffix,
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
-readability-redundant-member-init,
-readability-redundant-string-init,
-readability-avoid-const-params-in-decls,
-readability-named-parameter,
-readability-convert-member-functions-to-static,
-readability-qualified-auto,
-readability-make-member-function-const,
-readability-isolate-declaration,
-readability-inconsistent-declaration-parameter-name,
-clang-diagnostic-error,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true
performance-inefficient-string-concatenation.StrictMode: true
readability-braces-around-statements.ShortStatementLines: 0
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.ClassIgnoredRegexp: I.*
readability-identifier-naming.ClassPrefix: C # We can't use regex here?!?!?!?
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.EnumPrefix: e
readability-identifier-naming.EnumConstantCase: UPPER_CASE
readability-identifier-naming.FunctionCase: camelBack
readability-identifier-naming.NamespaceCase: CamelCase
readability-identifier-naming.NamespacePrefix: N
readability-identifier-naming.StructPrefix: S
readability-identifier-naming.StructCase: CamelCase

View file

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

View file

@ -13,8 +13,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Install Nix
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

3
.gitignore vendored
View file

@ -12,4 +12,5 @@ _deps
.vscode
build/
doxygen/
doxygen-awesome-css/
doxygen-awesome-css/
.cache/

View file

@ -28,9 +28,16 @@ endif()
add_compile_definitions(HYPRLANG_INTERNAL)
set(CMAKE_CXX_STANDARD 23)
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Wno-unused-parameter
-Wno-missing-field-initializers)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
find_package(PkgConfig REQUIRED)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprutils>=0.1.1)
pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprutils>=0.7.1)
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/hyprlang.hpp")
@ -47,15 +54,6 @@ set_target_properties(
target_link_libraries(hyprlang PkgConfig::deps)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# for std::expected. probably evil. Arch's clang is very outdated tho...
target_compile_options(hyprlang PUBLIC -std=gnu++2b -D__cpp_concepts=202002L
-Wno-macro-redefined)
add_compile_options(-stdlib=libc++)
add_link_options(-stdlib=libc++)
message(STATUS "Using clang++ to compile hyprlang")
endif()
add_library(hypr::hyprlang ALIAS hyprlang)
install(TARGETS hyprlang)

View file

@ -50,8 +50,8 @@ add_baker = Koichi, 18, Morioh
## Docs
Visit [hyprland.org/hyprlang](https://hyprland.org/hyprlang) to see the documentation.
Visit [wiki.hypr.land/Hypr-Ecosystem/hyprlang/](https://wiki.hypr.land/Hypr-Ecosystem/hyprlang/) to see the documentation.
### Example implementation
For an example implmentation, take a look at the `tests/` directory.
For an example implementation, take a look at the `tests/` directory.

View file

@ -1 +1 @@
0.5.3
0.6.7

12
flake.lock generated
View file

@ -10,11 +10,11 @@
]
},
"locked": {
"lastModified": 1721324102,
"narHash": "sha256-WAZ0X6yJW1hFG6otkHBfyJDKRpNP5stsRqdEuHrFRpk=",
"lastModified": 1749135356,
"narHash": "sha256-Q8mAKMDsFbCEuq7zoSlcTuxgbIBVhfIYpX0RjE32PS0=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "962582a090bc233c4de9d9897f46794280288989",
"rev": "e36db00dfb3a3d3fdcc4069cb292ff60d2699ccb",
"type": "github"
},
"original": {
@ -25,11 +25,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1721138476,
"narHash": "sha256-+W5eZOhhemLQxelojLxETfbFbc19NWawsXBlapYpqIA=",
"lastModified": 1748929857,
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ad0b5eed1b6031efaed382844806550c3dcb4206",
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
"type": "github"
},
"original": {

View file

@ -39,7 +39,7 @@
inputs.hyprutils.overlays.default
(final: prev: {
hyprlang = final.callPackage ./nix/default.nix {
stdenv = final.gcc13Stdenv;
stdenv = final.gcc15Stdenv;
version = version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty");
};
hyprlang-with-tests = final.hyprlang.override {doCheck = true;};

View file

@ -3,11 +3,13 @@
#ifndef HYPRLANG_HPP
#define HYPRLANG_HPP
#include <typeindex>
#include <any>
#include <memory>
#include <string>
#include <fstream>
#include <ostream>
#include <vector>
#include <print>
#include <cstdlib>
class CConfigImpl;
struct SConfigDefaultValue;
@ -48,7 +50,7 @@ namespace Hyprlang {
typedef CConfigCustomValueType CUSTOMTYPE;
/*!
A very simple vector type
A very simple vector type
*/
struct SVector2D {
float x = 0, y = 0;
@ -93,12 +95,12 @@ namespace Hyprlang {
Generic struct for options for the config parser
*/
struct SConfigOptions {
/*!
/*!
Don't throw errors on missing values.
*/
int verifyOnly = false;
/*!
/*!
Return all errors instead of just the first
*/
int throwAllErrors = false;
@ -173,11 +175,11 @@ namespace Hyprlang {
typedef void (*PCONFIGCUSTOMVALUEDESTRUCTOR)(void** data);
/*!
Container for a custom config value type
Container for a custom config value type
When creating, pass your handler.
Handler will receive a void** that points to a void* that you can set to your own
thing. Pass a dtor to free whatever you allocated when the custom value type is being released.
data may always be pointing to a nullptr.
data may always be pointing to a nullptr.
*/
class CConfigCustomValueType {
public:
@ -252,7 +254,7 @@ namespace Hyprlang {
/*!
Get the contained value as an std::any.
For strings, this is a const char*.
For custom data types, this is a CConfigCustomValueType*.
For custom data types, this is a void* representing the data ptr stored by it.
*/
std::any getValue() const {
switch (m_eType) {
@ -269,7 +271,7 @@ namespace Hyprlang {
/*!
\since 0.3.0
a flag to notify whether this value has been set explicitly by the user,
or not.
*/
@ -303,7 +305,7 @@ namespace Hyprlang {
~CConfig();
/*!
Add a config value, for example myCategory:myValue.
Add a config value, for example myCategory:myValue.
This has to be done before commence()
Value provided becomes default.
*/
@ -317,8 +319,8 @@ namespace Hyprlang {
/*!
\since 0.3.0
Unregister a handler.
Unregister a handler.
*/
void unregisterHandler(const char* name);
@ -360,14 +362,14 @@ namespace Hyprlang {
CParseResult parse();
/*!
Same as parse(), but parse a specific file, without any refreshing.
Same as parse(), but parse a specific file, without any refreshing.
recommended to use for stuff like source = path.conf
*/
CParseResult parseFile(const char* file);
/*!
Parse a single "line", dynamically.
Values set by this are temporary and will be overwritten
Parse a single "line", dynamically.
Values set by this are temporary and will be overwritten
by default / config on the next parse()
*/
CParseResult parseDynamic(const char* line);
@ -375,14 +377,14 @@ namespace Hyprlang {
/*!
Get a config's value ptr. These are static.
nullptr on fail
nullptr on fail
*/
CConfigValue* getConfigValuePtr(const char* name);
/*!
Get a special category's config value ptr. These are only static for static (key-less)
categories.
key can be nullptr for static categories. Cannot be nullptr for id-based categories.
key can be nullptr for static categories. Cannot be nullptr for id-based categories.
nullptr on fail.
*/
CConfigValue* getSpecialConfigValuePtr(const char* category, const char* name, const char* key = nullptr);
@ -439,23 +441,111 @@ namespace Hyprlang {
return result;
}
/*!
Change the root path of the config
\since 0.6.7
*/
void changeRootPath(const char* path);
private:
bool m_bCommenced = false;
bool m_bCommenced = false;
CConfigImpl* impl;
CConfigImpl* impl;
CParseResult parseLine(std::string line, bool dynamic = false);
CParseResult configSetValueSafe(const std::string& command, const std::string& value);
CParseResult parseVariable(const std::string& lhs, const std::string& rhs, bool dynamic = false);
void clearState();
void applyDefaultsToCat(SSpecialCategory& cat);
void retrieveKeysForCat(const char* category, const char*** out, size_t* len);
CParseResult parseRawStream(const std::string& stream);
CParseResult parseLine(std::string line, bool dynamic = false);
std::pair<bool, CParseResult> configSetValueSafe(const std::string& command, const std::string& value);
CParseResult parseVariable(const std::string& lhs, const std::string& rhs, bool dynamic = false);
void clearState();
void applyDefaultsToCat(SSpecialCategory& cat);
void retrieveKeysForCat(const char* category, const char*** out, size_t* len);
CParseResult parseRawStream(const std::string& stream);
};
/*!
Templated wrapper for Hyprlang values. Much more straightforward to use.
\since 0.6.0
*/
template <typename T>
class CSimpleConfigValue {
public:
CSimpleConfigValue(CConfig* const pConfig, const char* val) {
const auto VAL = pConfig->getConfigValuePtr(val);
if (!VAL) {
std::println("CSimpleConfigValue: value not found");
abort();
}
// NOLINTNEXTLINE
p_ = VAL->getDataStaticPtr();
#ifdef HYPRLAND_DEBUG
// verify type
const auto ANY = VAL->getValue();
const auto TYPE = std::type_index(ANY.type());
// exceptions
const bool STRINGEX = (typeid(T) == typeid(std::string) && TYPE == typeid(Hyprlang::STRING));
const bool CUSTOMEX = (typeid(T) == typeid(Hyprlang::CUSTOMTYPE) && (TYPE == typeid(Hyprlang::CUSTOMTYPE*) || TYPE == typeid(void*) /* dunno why it does this? */));
if (typeid(T) != TYPE && !STRINGEX && !CUSTOMEX) {
std::println("CSimpleConfigValue: Mismatched type in CConfigValue<T>, got {} but has {}", typeid(T).name(), TYPE.name());
abort();
}
#endif
}
T* ptr() const {
return *(T* const*)p_;
}
T operator*() const {
return *ptr();
}
private:
void* const* p_ = nullptr;
};
template <>
inline std::string* CSimpleConfigValue<std::string>::ptr() const {
std::print("Impossible to implement ptr() of CConfigValue<std::string>");
abort();
return nullptr;
}
template <>
inline std::string CSimpleConfigValue<std::string>::operator*() const {
return std::string{*(Hyprlang::STRING*)p_};
}
template <>
inline Hyprlang::STRING* CSimpleConfigValue<Hyprlang::STRING>::ptr() const {
return (Hyprlang::STRING*)p_;
}
template <>
inline Hyprlang::STRING CSimpleConfigValue<Hyprlang::STRING>::operator*() const {
return *(Hyprlang::STRING*)p_;
}
template <>
inline Hyprlang::CUSTOMTYPE* CSimpleConfigValue<Hyprlang::CUSTOMTYPE>::ptr() const {
return *(Hyprlang::CUSTOMTYPE* const*)p_;
}
template <>
inline Hyprlang::CUSTOMTYPE CSimpleConfigValue<Hyprlang::CUSTOMTYPE>::operator*() const {
std::print("Impossible to implement operator* of CConfigValue<Hyprlang::CUSTOMTYPE>, use ptr()");
abort();
return *ptr();
}
};
#ifndef HYPRLANG_INTERNAL
#undef HYPRLANG_END_MAGIC
#endif
#endif
#endif

View file

@ -1,6 +1,6 @@
#include "public.hpp"
#include "config.hpp"
#include <string.h>
#include <cstring>
using namespace Hyprlang;
@ -30,38 +30,28 @@ CConfigValue::~CConfigValue() {
}
}
CConfigValue::CConfigValue(const int64_t value) {
m_pData = new int64_t;
CConfigValue::CConfigValue(const int64_t value) : m_eType(CONFIGDATATYPE_INT), m_pData(new int64_t) {
*reinterpret_cast<int64_t*>(m_pData) = value;
m_eType = CONFIGDATATYPE_INT;
}
CConfigValue::CConfigValue(const float value) {
m_pData = new float;
CConfigValue::CConfigValue(const float value) : m_eType(CONFIGDATATYPE_FLOAT), m_pData(new float) {
*reinterpret_cast<float*>(m_pData) = value;
m_eType = CONFIGDATATYPE_FLOAT;
}
CConfigValue::CConfigValue(const SVector2D value) {
m_pData = new SVector2D;
CConfigValue::CConfigValue(const SVector2D value) : m_eType(CONFIGDATATYPE_VEC2), m_pData(new SVector2D) {
*reinterpret_cast<SVector2D*>(m_pData) = value;
m_eType = CONFIGDATATYPE_VEC2;
}
CConfigValue::CConfigValue(const char* value) {
m_pData = new char[strlen(value) + 1];
CConfigValue::CConfigValue(const char* value) : m_eType(CONFIGDATATYPE_STR), m_pData(new char[strlen(value) + 1]) {
strncpy((char*)m_pData, value, strlen(value));
((char*)m_pData)[strlen(value)] = '\0';
m_eType = CONFIGDATATYPE_STR;
}
CConfigValue::CConfigValue(CConfigCustomValueType&& value) {
m_pData = new CConfigCustomValueType(value);
m_eType = CONFIGDATATYPE_CUSTOM;
CConfigValue::CConfigValue(CConfigCustomValueType&& value) : m_eType(CONFIGDATATYPE_CUSTOM), m_pData(new CConfigCustomValueType(value)) {
;
}
CConfigValue::CConfigValue(const CConfigValue& other) {
m_eType = other.m_eType;
CConfigValue::CConfigValue(const CConfigValue& other) : m_eType(other.m_eType) {
setFrom(&other);
}
@ -77,11 +67,9 @@ void* const* CConfigValue::getDataStaticPtr() const {
return &m_pData;
}
CConfigCustomValueType::CConfigCustomValueType(PCONFIGCUSTOMVALUEHANDLERFUNC handler_, PCONFIGCUSTOMVALUEDESTRUCTOR dtor_, const char* def) {
handler = handler_;
dtor = dtor_;
defaultVal = def;
lastVal = def;
CConfigCustomValueType::CConfigCustomValueType(PCONFIGCUSTOMVALUEHANDLERFUNC handler_, PCONFIGCUSTOMVALUEDESTRUCTOR dtor_, const char* def) :
handler(handler_), dtor(dtor_), defaultVal(def), lastVal(def) {
;
}
CConfigCustomValueType::~CConfigCustomValueType() {
@ -216,4 +204,4 @@ void CConfigValue::setFrom(std::any ref) {
throw "bad defaultFrom type";
}
}
}
}

View file

@ -1,7 +1,9 @@
#include "config.hpp"
#include <array>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <format>
@ -12,6 +14,7 @@
#include <cstring>
#include <hyprutils/string/VarList.hpp>
#include <hyprutils/string/String.hpp>
#include <hyprutils/string/ConstVarList.hpp>
using namespace Hyprlang;
using namespace Hyprutils::String;
@ -20,28 +23,51 @@ using namespace Hyprutils::String;
#include <crt_externs.h>
#define environ (*_NSGetEnviron())
#else
// NOLINTNEXTLINE
extern "C" char** environ;
#endif
// defines
inline constexpr const char* ANONYMOUS_KEY = "__hyprlang_internal_anonymous_key";
inline constexpr const char* ANONYMOUS_KEY = "__hyprlang_internal_anonymous_key";
inline constexpr const char* MULTILINE_SPACE_CHARSET = " \t";
//
static size_t seekABIStructSize(const void* begin, size_t startOffset, size_t maxSize) {
for (size_t off = startOffset; off < maxSize; off += 4) {
if (*(int*)((unsigned char*)begin + off) == int{HYPRLANG_END_MAGIC})
if (*(int*)((unsigned char*)begin + off) == HYPRLANG_END_MAGIC)
return off;
}
return 0;
}
CConfig::CConfig(const char* path, const Hyprlang::SConfigOptions& options_) {
static std::expected<std::string, eGetNextLineFailure> getNextLine(std::istream& str, int& rawLineNum, int& lineNum) {
std::string line = "";
std::string nextLine = "";
if (!std::getline(str, line))
return std::unexpected(GETNEXTLINEFAILURE_EOF);
lineNum = ++rawLineNum;
while (line.length() > 0 && line.at(line.length() - 1) == '\\') {
const auto lastNonSpace = line.length() < 2 ? -1 : line.find_last_not_of(MULTILINE_SPACE_CHARSET, line.length() - 2);
line = line.substr(0, lastNonSpace + 1);
if (!std::getline(str, nextLine))
return std::unexpected(GETNEXTLINEFAILURE_BACKSLASH);
++rawLineNum;
line += nextLine;
}
return line;
}
CConfig::CConfig(const char* path, const Hyprlang::SConfigOptions& options_) : impl(new CConfigImpl) {
SConfigOptions options;
std::memcpy(&options, &options_, seekABIStructSize(&options_, 16, sizeof(SConfigOptions)));
impl = new CConfigImpl;
if (options.pathIsStream)
impl->rawConfigString = path;
else
@ -52,15 +78,9 @@ CConfig::CConfig(const char* path, const Hyprlang::SConfigOptions& options_) {
throw "File does not exist";
}
impl->envVariables.clear();
for (char** env = environ; *env; ++env) {
const std::string ENVVAR = *env ? *env : "";
const auto VARIABLE = ENVVAR.substr(0, ENVVAR.find_first_of('='));
const auto VALUE = ENVVAR.substr(ENVVAR.find_first_of('=') + 1);
impl->envVariables.push_back({VARIABLE, VALUE});
}
impl->recheckEnv();
std::sort(impl->envVariables.begin(), impl->envVariables.end(), [&](const auto& a, const auto& b) { return a.name.length() > b.name.length(); });
std::ranges::sort(impl->envVariables, [&](const auto& a, const auto& b) { return a.name.length() > b.name.length(); });
impl->configOptions = options;
}
@ -74,40 +94,42 @@ void CConfig::addConfigValue(const char* name, const CConfigValue& value) {
throw "Cannot addConfigValue after commence()";
if ((eDataType)value.m_eType != CONFIGDATATYPE_CUSTOM && (eDataType)value.m_eType != CONFIGDATATYPE_STR)
impl->defaultValues.emplace(name, SConfigDefaultValue{value.getValue(), (eDataType)value.m_eType});
impl->defaultValues.emplace(name, SConfigDefaultValue{.data = value.getValue(), .type = (eDataType)value.m_eType});
else if ((eDataType)value.m_eType == CONFIGDATATYPE_STR)
impl->defaultValues.emplace(name, SConfigDefaultValue{std::string{std::any_cast<const char*>(value.getValue())}, (eDataType)value.m_eType});
impl->defaultValues.emplace(name, SConfigDefaultValue{.data = std::string{std::any_cast<const char*>(value.getValue())}, .type = (eDataType)value.m_eType});
else
impl->defaultValues.emplace(name,
SConfigDefaultValue{reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->defaultVal, (eDataType)value.m_eType,
reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->handler,
reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->dtor});
SConfigDefaultValue{.data = reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->defaultVal,
.type = (eDataType)value.m_eType,
.handler = reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->handler,
.dtor = reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->dtor});
}
void CConfig::addSpecialConfigValue(const char* cat, const char* name, const CConfigValue& value) {
const auto IT = std::find_if(impl->specialCategoryDescriptors.begin(), impl->specialCategoryDescriptors.end(), [&](const auto& other) { return other->name == cat; });
const auto IT = std::ranges::find_if(impl->specialCategoryDescriptors, [&](const auto& other) { return other->name == cat; });
if (IT == impl->specialCategoryDescriptors.end())
throw "No such category";
if ((eDataType)value.m_eType != CONFIGDATATYPE_CUSTOM && (eDataType)value.m_eType != CONFIGDATATYPE_STR)
IT->get()->defaultValues.emplace(name, SConfigDefaultValue{value.getValue(), (eDataType)value.m_eType});
IT->get()->defaultValues.emplace(name, SConfigDefaultValue{.data = value.getValue(), .type = (eDataType)value.m_eType});
else if ((eDataType)value.m_eType == CONFIGDATATYPE_STR)
IT->get()->defaultValues.emplace(name, SConfigDefaultValue{std::string{std::any_cast<const char*>(value.getValue())}, (eDataType)value.m_eType});
IT->get()->defaultValues.emplace(name, SConfigDefaultValue{.data = std::string{std::any_cast<const char*>(value.getValue())}, .type = (eDataType)value.m_eType});
else
IT->get()->defaultValues.emplace(name,
SConfigDefaultValue{reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->defaultVal, (eDataType)value.m_eType,
reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->handler,
reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->dtor});
SConfigDefaultValue{.data = reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->defaultVal,
.type = (eDataType)value.m_eType,
.handler = reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->handler,
.dtor = reinterpret_cast<CConfigCustomValueType*>(value.m_pData)->dtor});
const auto CAT = std::find_if(impl->specialCategories.begin(), impl->specialCategories.end(), [cat, name](const auto& other) { return other->name == cat && other->isStatic; });
const auto CAT = std::ranges::find_if(impl->specialCategories, [cat](const auto& other) { return other->name == cat && other->isStatic; });
if (CAT != impl->specialCategories.end())
CAT->get()->values[name].defaultFrom(IT->get()->defaultValues[name]);
}
void CConfig::removeSpecialConfigValue(const char* cat, const char* name) {
const auto IT = std::find_if(impl->specialCategoryDescriptors.begin(), impl->specialCategoryDescriptors.end(), [&](const auto& other) { return other->name == cat; });
const auto IT = std::ranges::find_if(impl->specialCategoryDescriptors, [&](const auto& other) { return other->name == cat; });
if (IT == impl->specialCategoryDescriptors.end())
throw "No such category";
@ -138,9 +160,8 @@ void CConfig::addSpecialCategory(const char* name, SSpecialCategoryOptions optio
}
// sort longest to shortest
std::sort(impl->specialCategories.begin(), impl->specialCategories.end(), [](const auto& a, const auto& b) -> int { return a->name.length() > b->name.length(); });
std::sort(impl->specialCategoryDescriptors.begin(), impl->specialCategoryDescriptors.end(),
[](const auto& a, const auto& b) -> int { return a->name.length() > b->name.length(); });
std::ranges::sort(impl->specialCategories, [](const auto& a, const auto& b) -> int { return a->name.length() > b->name.length(); });
std::ranges::sort(impl->specialCategoryDescriptors, [](const auto& a, const auto& b) -> int { return a->name.length() > b->name.length(); });
}
void CConfig::removeSpecialCategory(const char* name) {
@ -195,7 +216,7 @@ static std::expected<int64_t, std::string> configStringToInt(const std::string&
if (!r.has_value() || !g.has_value() || !b.has_value())
return std::unexpected("failed parsing " + VALUEWITHOUTFUNC);
return a * (Hyprlang::INT)0x1000000 + r.value() * (Hyprlang::INT)0x10000 + g.value() * (Hyprlang::INT)0x100 + b.value();
return (a * (Hyprlang::INT)0x1000000) + (r.value() * (Hyprlang::INT)0x10000) + (g.value() * (Hyprlang::INT)0x100) + b.value();
} else if (VALUEWITHOUTFUNC.length() == 8) {
const auto RGBA = parseHex(VALUEWITHOUTFUNC);
@ -203,7 +224,7 @@ static std::expected<int64_t, std::string> configStringToInt(const std::string&
return RGBA;
// now we need to RGBA -> ARGB. The config holds ARGB only.
return (RGBA.value() >> 8) + 0x1000000 * (RGBA.value() & 0xFF);
return (RGBA.value() >> 8) + (0x1000000 * (RGBA.value() & 0xFF));
}
return std::unexpected("rgba() expects length of 8 characters (4 bytes) or 4 comma separated values");
@ -224,7 +245,7 @@ static std::expected<int64_t, std::string> configStringToInt(const std::string&
if (!r.has_value() || !g.has_value() || !b.has_value())
return std::unexpected("failed parsing " + VALUEWITHOUTFUNC);
return (Hyprlang::INT)0xFF000000 + r.value() * (Hyprlang::INT)0x10000 + g.value() * (Hyprlang::INT)0x100 + b.value();
return (Hyprlang::INT)0xFF000000 + (r.value() * (Hyprlang::INT)0x10000) + (g.value() * (Hyprlang::INT)0x100) + b.value();
} else if (VALUEWITHOUTFUNC.length() == 6) {
const auto RGB = parseHex(VALUEWITHOUTFUNC);
@ -252,21 +273,21 @@ static std::expected<int64_t, std::string> configStringToInt(const std::string&
return 0;
}
CParseResult CConfig::configSetValueSafe(const std::string& command, const std::string& value) {
// found, result
std::pair<bool, CParseResult> CConfig::configSetValueSafe(const std::string& command, const std::string& value) {
CParseResult result;
std::string valueName;
std::string catPrefix;
for (auto& c : impl->categories) {
valueName += c + ':';
catPrefix += c + ':';
}
valueName += command;
const auto VALUEONLYNAME = command.starts_with(catPrefix) ? command.substr(catPrefix.length()) : command;
// TODO: all this sucks xD
SSpecialCategory* overrideSpecialCat = nullptr;
// FIXME: this will bug with nested.
if (valueName.contains('[') && valueName.contains(']')) {
const auto L = valueName.find_first_of('[');
const auto R = valueName.find_last_of(']');
@ -277,12 +298,27 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
valueName = valueName.substr(0, L) + valueName.substr(R + 1);
// if it doesn't exist, make it
for (auto& sc : impl->specialCategoryDescriptors) {
if (sc->key.empty() || !valueName.starts_with(sc->name))
if (sc->key.empty() || !valueName.starts_with(sc->name + ":"))
continue;
// bingo
bool keyExists = false;
for (const auto& specialCat : impl->specialCategories) {
if (specialCat->key != sc->key || specialCat->name != sc->name)
continue;
if (CATKEY != std::string_view{std::any_cast<const char*>(specialCat->values[sc->key].getValue())})
continue;
// existing special
keyExists = true;
overrideSpecialCat = specialCat.get();
}
if (keyExists)
break;
// if it doesn't exist, make it
const auto PCAT = impl->specialCategories.emplace_back(std::make_unique<SSpecialCategory>()).get();
PCAT->descriptor = sc.get();
PCAT->name = sc->name;
@ -292,6 +328,8 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
applyDefaultsToCat(*PCAT);
PCAT->values[sc->key].setFrom(CATKEY);
overrideSpecialCat = PCAT;
break;
}
}
}
@ -301,86 +339,99 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
// it might be in a special category
bool found = false;
if (impl->currentSpecialCategory && valueName.starts_with(impl->currentSpecialCategory->name)) {
VALUEIT = impl->currentSpecialCategory->values.find(valueName.substr(impl->currentSpecialCategory->name.length() + 1));
if (overrideSpecialCat) {
VALUEIT = overrideSpecialCat->values.find(valueName.substr(overrideSpecialCat->name.length() + 1));
if (VALUEIT != impl->currentSpecialCategory->values.end())
if (VALUEIT != overrideSpecialCat->values.end())
found = true;
}
} else {
if (impl->currentSpecialCategory && valueName.starts_with(impl->currentSpecialCategory->name)) {
VALUEIT = impl->currentSpecialCategory->values.find(valueName.substr(impl->currentSpecialCategory->name.length() + 1));
if (!found) {
for (auto& sc : impl->specialCategories) {
if (!valueName.starts_with(sc->name))
continue;
if (!sc->isStatic && std::string{std::any_cast<const char*>(sc->values[sc->key].getValue())} != impl->currentSpecialKey)
continue;
VALUEIT = sc->values.find(valueName.substr(sc->name.length() + 1));
impl->currentSpecialCategory = sc.get();
if (VALUEIT != sc->values.end())
if (VALUEIT != impl->currentSpecialCategory->values.end())
found = true;
else if (sc->descriptor->dontErrorOnMissing)
return result; // will return a success, cuz we want to ignore missing
break;
}
}
if (!found) {
// could be a dynamic category that doesnt exist yet
for (auto& sc : impl->specialCategoryDescriptors) {
if (sc->key.empty() || !valueName.starts_with(sc->name))
continue;
// probably a handler
if (!valueName.contains(":"))
return {false, result};
if (!found) {
for (auto& sc : impl->specialCategories) {
if (!valueName.starts_with(sc->name + ":"))
continue;
if (!sc->isStatic && std::string{std::any_cast<const char*>(sc->values[sc->key].getValue())} != impl->currentSpecialKey)
continue;
VALUEIT = sc->values.find(valueName.substr(sc->name.length() + 1));
impl->currentSpecialCategory = sc.get();
if (VALUEIT != sc->values.end())
found = true;
else if (sc->descriptor->dontErrorOnMissing)
return {false, result}; // will return a success, cuz we want to ignore missing
// category does exist, check if value exists
if (!sc->defaultValues.contains(VALUEONLYNAME) && VALUEONLYNAME != sc->key)
break;
// bingo
const auto PCAT = impl->specialCategories.emplace_back(std::make_unique<SSpecialCategory>()).get();
PCAT->descriptor = sc.get();
PCAT->name = sc->name;
PCAT->key = sc->key;
addSpecialConfigValue(sc->name.c_str(), sc->key.c_str(), CConfigValue("0"));
applyDefaultsToCat(*PCAT);
VALUEIT = PCAT->values.find(valueName.substr(sc->name.length() + 1));
impl->currentSpecialCategory = PCAT;
if (VALUEIT != PCAT->values.end())
found = true;
if (sc->anonymous) {
// find suitable key
size_t biggest = 0;
for (auto& catt : impl->specialCategories) {
if (catt->anonymousID > biggest)
biggest = catt->anonymousID;
}
biggest++;
PCAT->values[ANONYMOUS_KEY].setFrom(std::to_string(biggest));
impl->currentSpecialKey = std::to_string(biggest);
PCAT->anonymousID = biggest;
} else {
if (VALUEIT == PCAT->values.end() || VALUEIT->first != sc->key) {
result.setError(std::format("special category's first value must be the key. Key for <{}> is <{}>", PCAT->name, PCAT->key));
return result;
}
impl->currentSpecialKey = value;
}
}
break;
if (!found) {
// could be a dynamic category that doesnt exist yet
for (auto& sc : impl->specialCategoryDescriptors) {
if (sc->key.empty() || !valueName.starts_with(sc->name + ":"))
continue;
// found value root to be a special category, get the trunk
const auto VALUETRUNK = valueName.substr(sc->name.length() + 1);
// check if trunk is a value within the special category
if (!sc->defaultValues.contains(VALUETRUNK) && VALUETRUNK != sc->key)
break;
// bingo
const auto PCAT = impl->specialCategories.emplace_back(std::make_unique<SSpecialCategory>()).get();
PCAT->descriptor = sc.get();
PCAT->name = sc->name;
PCAT->key = sc->key;
addSpecialConfigValue(sc->name.c_str(), sc->key.c_str(), CConfigValue("0"));
applyDefaultsToCat(*PCAT);
VALUEIT = PCAT->values.find(valueName.substr(sc->name.length() + 1));
impl->currentSpecialCategory = PCAT;
if (VALUEIT != PCAT->values.end())
found = true;
if (sc->anonymous) {
// find suitable key
size_t biggest = 0;
for (auto& catt : impl->specialCategories) {
biggest = std::max(catt->anonymousID, biggest);
}
biggest++;
PCAT->values[ANONYMOUS_KEY].setFrom(std::to_string(biggest));
impl->currentSpecialKey = std::to_string(biggest);
PCAT->anonymousID = biggest;
} else {
if (VALUEIT == PCAT->values.end() || VALUEIT->first != sc->key) {
result.setError(std::format("special category's first value must be the key. Key for <{}> is <{}>", PCAT->name, PCAT->key));
return {true, result};
}
impl->currentSpecialKey = value;
}
break;
}
}
}
if (!found) {
result.setError(std::format("config option <{}> does not exist.", valueName));
return result;
return {false, result};
}
}
@ -390,7 +441,7 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
const auto INT = configStringToInt(value);
if (!INT.has_value()) {
result.setError(INT.error());
return result;
return {true, result};
}
VALUEIT->second.setFrom(INT.value());
@ -402,7 +453,7 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
VALUEIT->second.setFrom(std::stof(value));
} catch (std::exception& e) {
result.setError(std::format("failed parsing a float: {}", e.what()));
return result;
return {true, result};
}
break;
}
@ -417,10 +468,10 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
if (LHS.contains(" ") || RHS.contains(" "))
throw std::runtime_error("too many args");
VALUEIT->second.setFrom(SVector2D{std::stof(LHS), std::stof(RHS)});
VALUEIT->second.setFrom(SVector2D{.x = std::stof(LHS), .y = std::stof(RHS)});
} catch (std::exception& e) {
result.setError(std::format("failed parsing a vec2: {}", e.what()));
return result;
return {true, result};
}
break;
}
@ -435,30 +486,30 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std::
if (RESULT.error) {
result.setError(RESULT.getError());
return result;
return {true, result};
}
break;
}
default: {
result.setError("internal error: invalid value found (no type?)");
return result;
return {true, result};
}
}
VALUEIT->second.m_bSetByUser = true;
return result;
return {true, result};
}
CParseResult CConfig::parseVariable(const std::string& lhs, const std::string& rhs, bool dynamic) {
auto IT = std::find_if(impl->variables.begin(), impl->variables.end(), [&](const auto& v) { return v.name == lhs.substr(1); });
auto IT = std::ranges::find_if(impl->variables, [&](const auto& v) { return v.name == lhs.substr(1); });
if (IT != impl->variables.end())
IT->value = rhs;
else {
impl->variables.push_back({lhs.substr(1), rhs});
std::sort(impl->variables.begin(), impl->variables.end(), [](const auto& lhs, const auto& rhs) { return lhs.name.length() > rhs.name.length(); });
IT = std::find_if(impl->variables.begin(), impl->variables.end(), [&](const auto& v) { return v.name == lhs.substr(1); });
std::ranges::sort(impl->variables, [](const auto& lhs, const auto& rhs) { return lhs.name.length() > rhs.name.length(); });
IT = std::ranges::find_if(impl->variables, [&](const auto& v) { return v.name == lhs.substr(1); });
}
if (dynamic) {
@ -475,16 +526,130 @@ CParseResult CConfig::parseVariable(const std::string& lhs, const std::string& r
return result;
}
void CConfigImpl::parseComment(const std::string& comment) {
void CConfigImpl::recheckEnv() {
envVariables.clear();
for (char** env = environ; *env; ++env) {
const std::string ENVVAR = *env ? *env : "";
const auto VARIABLE = ENVVAR.substr(0, ENVVAR.find_first_of('='));
const auto VALUE = ENVVAR.substr(ENVVAR.find_first_of('=') + 1);
envVariables.push_back({VARIABLE, VALUE});
}
}
SVariable* CConfigImpl::getVariable(const std::string& name) {
for (auto& v : envVariables) {
if (v.name == name)
return &v;
}
for (auto& v : variables) {
if (v.name == name)
return &v;
}
return nullptr;
}
std::optional<std::string> CConfigImpl::parseComment(const std::string& comment) {
const auto COMMENT = trim(comment);
if (!COMMENT.starts_with("hyprlang"))
return;
return std::nullopt;
CVarList args(COMMENT, 0, 's', true);
CConstVarList args(COMMENT, 0, 's', true);
if (args[1] == "noerror")
currentFlags.noError = args[2] == "true" || args[2] == "yes" || args[2] == "enable" || args[2] == "enabled" || args[2] == "set";
bool negated = false;
std::string ifBlockVariable = "";
for (size_t i = 1; i < args.size(); ++i) {
if (args[i] == "noerror") {
if (negated)
currentFlags.noError = false;
else
currentFlags.noError = args[2] == "true" || args[2] == "yes" || args[2] == "enable" || args[2] == "enabled" || args[2] == "set" || args[2].empty();
break;
}
if (args[i] == "endif") {
if (currentFlags.ifDatas.empty())
return "stray endif";
currentFlags.ifDatas.pop_back();
break;
}
if (args[i] == "if") {
ifBlockVariable = args[++i];
break;
}
}
if (!ifBlockVariable.empty()) {
if (ifBlockVariable.starts_with("!")) {
negated = true;
ifBlockVariable = ifBlockVariable.substr(1);
}
CConfigImpl::SIfBlockData newIfData;
if (const auto VAR = getVariable(ifBlockVariable); VAR)
newIfData.failed = negated ? VAR->truthy() : !VAR->truthy();
else
newIfData.failed = !negated;
currentFlags.ifDatas.emplace_back(newIfData);
}
return std::nullopt;
}
std::expected<float, std::string> CConfigImpl::parseExpression(const std::string& s) {
// for now, we only support very basic expressions.
// + - * / and only one per $()
// TODO: something better
if (s.empty())
return std::unexpected("Expression is empty");
CConstVarList args(s, 0, 's', true);
if (args[1] != "+" && args[1] != "-" && args[1] != "*" && args[1] != "/")
return std::unexpected("Invalid expression type: supported +, -, *, /");
auto LHS_VAR = std::ranges::find_if(variables, [&](const auto& v) { return v.name == args[0]; });
auto RHS_VAR = std::ranges::find_if(variables, [&](const auto& v) { return v.name == args[2]; });
float left = 0;
float right = 0;
if (LHS_VAR != variables.end()) {
try {
left = std::stof(LHS_VAR->value);
} catch (...) { return std::unexpected("Failed to parse expression: value 1 holds a variable that does not look like a number"); }
} else {
try {
left = std::stof(std::string{args[0]});
} catch (...) { return std::unexpected("Failed to parse expression: value 1 does not look like a number or the variable doesn't exist"); }
}
if (RHS_VAR != variables.end()) {
try {
right = std::stof(RHS_VAR->value);
} catch (...) { return std::unexpected("Failed to parse expression: value 1 holds a variable that does not look like a number"); }
} else {
try {
right = std::stof(std::string{args[2]});
} catch (...) { return std::unexpected("Failed to parse expression: value 1 does not look like a number or the variable doesn't exist"); }
}
switch (args[1][0]) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
default: break;
}
return std::unexpected("Unknown error while parsing expression");
}
CParseResult CConfig::parseLine(std::string line, bool dynamic) {
@ -495,10 +660,15 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) {
auto commentPos = line.find('#');
if (commentPos == 0) {
impl->parseComment(line.substr(1));
const auto COMMENT_RESULT = impl->parseComment(line.substr(1));
if (COMMENT_RESULT.has_value())
result.setError(*COMMENT_RESULT);
return result;
}
if (!impl->currentFlags.ifDatas.empty() && impl->currentFlags.ifDatas.back().failed)
return result;
size_t lastHashPos = 0;
while (commentPos != std::string::npos) {
@ -548,6 +718,8 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) {
// limit unwrapping iterations to 100. if exceeds, raise error
for (size_t i = 0; i < 100; ++i) {
bool anyMatch = false;
// parse variables
for (auto& var : impl->variables) {
// don't parse LHS variables if this is a variable...
const auto LHSIT = ISVARIABLE ? std::string::npos : LHS.find("$" + var.name);
@ -566,6 +738,47 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) {
anyMatch = true;
}
// parse expressions {{somevar + 2}}
// We only support single expressions for now
while (RHS.contains("{{")) {
auto firstUnescaped = RHS.find("{{");
// Keep searching until non-escaped expression start is found
while (firstUnescaped > 0) {
// Special check to avoid undefined behaviour with std::basic_string::find_last_not_of
auto amountSkipped = 0;
for (int i = firstUnescaped - 1; i >= 0; i--) {
if (RHS.at(i) != '\\')
break;
amountSkipped++;
}
// No escape chars, or even escape chars. means they escaped themselves.
if (amountSkipped % 2 == 0)
break;
// Continue searching for next valid expression start.
firstUnescaped = RHS.find("{{", firstUnescaped + 1);
// Break if the next match is never found
if (firstUnescaped == std::string::npos)
break;
}
// Real match was never found.
if (firstUnescaped == std::string::npos)
break;
const auto BEGIN_EXPR = firstUnescaped;
// "}}" doesnt need escaping. Would be invalid expression anyways.
const auto END_EXPR = RHS.find("}}", BEGIN_EXPR + 2);
if (END_EXPR != std::string::npos) {
// try to parse the expression
const auto RESULT = impl->parseExpression(RHS.substr(BEGIN_EXPR + 2, END_EXPR - BEGIN_EXPR - 2));
if (!RESULT.has_value()) {
result.setError(RESULT.error());
return result;
}
RHS = RHS.substr(0, BEGIN_EXPR) + std::format("{}", RESULT.value()) + RHS.substr(END_EXPR + 2);
} else
break;
}
if (!anyMatch)
break;
@ -578,47 +791,74 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) {
if (ISVARIABLE)
return parseVariable(LHS, RHS, dynamic);
bool found = false;
for (auto& h : impl->handlers) {
// we want to handle potentially nested keywords and ensure
// we only call the handler if they are scoped correctly,
// unless the keyword is not scoped itself
const bool UNSCOPED = !h.name.contains(":");
const auto HANDLERNAME = !h.name.empty() && h.name.at(0) == ':' ? h.name.substr(1) : h.name;
if (!h.options.allowFlags && !UNSCOPED) {
size_t colon = 0;
size_t idx = 0;
size_t depth = 0;
while ((colon = HANDLERNAME.find(":", idx)) != std::string::npos && impl->categories.size() > depth) {
auto actual = HANDLERNAME.substr(idx, colon - idx);
if (actual != impl->categories[depth])
break;
idx = colon + 1;
++depth;
}
if (depth != impl->categories.size() || HANDLERNAME.substr(idx) != LHS)
continue;
// Removing escape chars. -- in the future, maybe map all the chars that can be escaped.
// Right now only expression parsing has escapeable chars
const char ESCAPE_CHAR = '\\';
const std::array<char, 2> ESCAPE_SET{'{', '}'};
for (size_t i = 0; RHS.length() != 0 && i < RHS.length() - 1; i++) {
if (RHS.at(i) != ESCAPE_CHAR)
continue;
//if escaping an escape, remove and skip the next char
if (RHS.at(i + 1) == ESCAPE_CHAR) {
RHS.erase(i, 1);
continue;
}
//checks if any of the chars were escapable.
for (const auto& ESCAPABLE_CHAR : ESCAPE_SET) {
if (RHS.at(i + 1) != ESCAPABLE_CHAR)
continue;
RHS.erase(i--, 1);
break;
}
if (UNSCOPED && HANDLERNAME != LHS && !h.options.allowFlags)
continue;
if (h.options.allowFlags && (!LHS.starts_with(HANDLERNAME) || LHS.contains(':') /* avoid cases where a category is called the same as a handler */))
continue;
ret = h.func(LHS.c_str(), RHS.c_str());
found = true;
}
if (!found && !impl->configOptions.verifyOnly)
ret = configSetValueSafe(LHS, RHS);
bool found = false;
if (!impl->configOptions.verifyOnly) {
auto [f, rv] = configSetValueSafe(LHS, RHS);
found = f;
ret = std::move(rv);
ret.errorString = ret.errorStdString.c_str();
}
if (!found) {
for (auto& h : impl->handlers) {
// we want to handle potentially nested keywords and ensure
// we only call the handler if they are scoped correctly,
// unless the keyword is not scoped itself
const bool UNSCOPED = !h.name.contains(":");
const auto HANDLERNAME = !h.name.empty() && h.name.at(0) == ':' ? h.name.substr(1) : h.name;
if (!h.options.allowFlags && !UNSCOPED) {
size_t colon = 0;
size_t idx = 0;
size_t depth = 0;
while ((colon = HANDLERNAME.find(':', idx)) != std::string::npos && impl->categories.size() > depth) {
auto actual = HANDLERNAME.substr(idx, colon - idx);
if (actual != impl->categories[depth])
break;
idx = colon + 1;
++depth;
}
if (depth != impl->categories.size() || HANDLERNAME.substr(idx) != LHS)
continue;
}
if (UNSCOPED && HANDLERNAME != LHS && !h.options.allowFlags)
continue;
if (h.options.allowFlags && (!LHS.starts_with(HANDLERNAME) || LHS.contains(':') /* avoid cases where a category is called the same as a handler */))
continue;
ret = h.func(LHS.c_str(), RHS.c_str());
found = true;
}
}
if (ret.error)
return ret;
@ -636,9 +876,12 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) {
return result;
}
impl->currentSpecialKey = "";
impl->currentSpecialCategory = nullptr;
impl->categories.pop_back();
if (impl->categories.empty()) {
impl->currentSpecialKey = "";
impl->currentSpecialCategory = nullptr;
}
} else {
// open a category.
if (!line.ends_with("{")) {
@ -692,25 +935,42 @@ CParseResult CConfig::parse() {
return fileParseResult;
}
void CConfig::changeRootPath(const char* path) {
impl->path = path;
}
CParseResult CConfig::parseRawStream(const std::string& stream) {
CParseResult result;
std::string line = "";
int linenum = 1;
int rawLineNum = 0;
int lineNum = 0;
std::stringstream str(stream);
while (std::getline(str, line)) {
const auto RET = parseLine(line);
while (true) {
const auto line = getNextLine(str, rawLineNum, lineNum);
if (!line) {
switch (line.error()) {
case GETNEXTLINEFAILURE_EOF: break;
case GETNEXTLINEFAILURE_BACKSLASH:
if (!impl->parseError.empty())
impl->parseError += "\n";
impl->parseError += std::format("Config error: Last line ends with backslash");
result.setError(impl->parseError);
break;
}
break;
}
const auto RET = parseLine(line.value());
if (RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) {
if (!impl->parseError.empty())
impl->parseError += "\n";
impl->parseError += std::format("Config error at line {}: {}", linenum, RET.errorStdString);
impl->parseError += std::format("Config error at line {}: {}", lineNum, RET.errorStdString);
result.setError(impl->parseError);
}
++linenum;
}
if (!impl->categories.empty()) {
@ -736,21 +996,33 @@ CParseResult CConfig::parseFile(const char* file) {
return result;
}
std::string line = "";
int linenum = 1;
int rawLineNum = 0;
int lineNum = 0;
while (std::getline(iffile, line)) {
while (true) {
const auto line = getNextLine(iffile, rawLineNum, lineNum);
const auto RET = parseLine(line);
if (!line) {
switch (line.error()) {
case GETNEXTLINEFAILURE_EOF: break;
case GETNEXTLINEFAILURE_BACKSLASH:
if (!impl->parseError.empty())
impl->parseError += "\n";
impl->parseError += std::format("Config error in file {}: Last line ends with backslash", file);
result.setError(impl->parseError);
break;
}
break;
}
const auto RET = parseLine(line.value());
if (!impl->currentFlags.noError && RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) {
if (!impl->parseError.empty())
impl->parseError += "\n";
impl->parseError += std::format("Config error in file {} at line {}: {}", file, linenum, RET.errorStdString);
impl->parseError += std::format("Config error in file {} at line {}: {}", file, lineNum, RET.errorStdString);
result.setError(impl->parseError);
}
++linenum;
}
iffile.close();
@ -770,17 +1042,22 @@ CParseResult CConfig::parseFile(const char* file) {
}
CParseResult CConfig::parseDynamic(const char* line) {
return parseLine(line, true);
auto ret = parseLine(line, true);
impl->currentSpecialCategory = nullptr;
return ret;
}
CParseResult CConfig::parseDynamic(const char* command, const char* value) {
return parseLine(std::string{command} + "=" + std::string{value}, true);
auto ret = parseLine(std::string{command} + "=" + std::string{value}, true);
impl->currentSpecialCategory = nullptr;
return ret;
}
void CConfig::clearState() {
impl->categories.clear();
impl->parseError = "";
impl->variables = impl->envVariables;
impl->recheckEnv();
impl->variables = impl->envVariables;
std::erase_if(impl->specialCategories, [](const auto& e) { return !e->isStatic; });
}
@ -811,7 +1088,7 @@ CConfigValue* CConfig::getSpecialConfigValuePtr(const char* category, const char
void CConfig::registerHandler(PCONFIGHANDLERFUNC func, const char* name, SHandlerOptions options_) {
SHandlerOptions options;
std::memcpy(&options, &options_, seekABIStructSize(&options_, 0, sizeof(SHandlerOptions)));
impl->handlers.push_back(SHandler{name, options, func});
impl->handlers.push_back(SHandler{.name = name, .options = options, .func = func});
}
void CConfig::unregisterHandler(const char* name) {

View file

@ -3,6 +3,8 @@
#include <unordered_map>
#include <string>
#include <vector>
#include <memory>
#include <expected>
struct SHandler {
std::string name = "";
@ -21,6 +23,10 @@ struct SVariable {
};
std::vector<SVarLine> linesContainingVar; // for dynamic updates
bool truthy() {
return value.length() > 0;
}
};
// remember to also edit CConfigValue if editing
@ -64,6 +70,11 @@ struct SSpecialCategory {
size_t anonymousID = 0;
};
enum eGetNextLineFailure : uint8_t {
GETNEXTLINEFAILURE_EOF = 0,
GETNEXTLINEFAILURE_BACKSLASH,
};
class CConfigImpl {
public:
std::string path = "";
@ -88,9 +99,18 @@ class CConfigImpl {
Hyprlang::SConfigOptions configOptions;
void parseComment(const std::string& comment);
std::optional<std::string> parseComment(const std::string& comment);
std::expected<float, std::string> parseExpression(const std::string& s);
SVariable* getVariable(const std::string& name);
void recheckEnv();
struct SIfBlockData {
bool failed = false;
};
struct {
bool noError = false;
bool noError = false;
std::vector<SIfBlockData> ifDatas;
} currentFlags;
};
};

View file

@ -14,23 +14,65 @@ $MY_VAR = 1337
$MY_VAR_2 = $MY_VAR
testVar = $MY_VAR$MY_VAR_2
$EXPR_VAR = {{MY_VAR + 2}}
testExpr = {{EXPR_VAR - 4}}
testEscapedExpr = \{{testInt + 7}}
testEscapedExpr2 = {\{testInt + 7}}
testEscapedExpr3 = \{\{3 + 8}}
testEscapedEscape = \\{{10 - 5}}
testMixedEscapedExpression = {{8 - 10}} \{{ \{{50 + 50}} / \{{10 * 5}} }}
testMixedEscapedExpression2 = {\{8\\{{10 + 3}}}} should equal "\{{8\13}}"
$ESCAPED_TEXT = \{{10 + 10}}
testImbeddedEscapedExpression = $ESCAPED_TEXT
$MOVING_VAR = 1000
$DYNAMIC_EXPRESSION = moved: {{$MOVING_VAR / 2}} expr: \{{$MOVING_VAR / 2}}
testDynamicEscapedExpression = \{{ $DYNAMIC_EXPRESSION }}
testEnv = $SHELL
testEnv2 = $TEST_ENV
source = ./colors.conf
customType = abc
# hyprlang if !NONEXISTENT_VAR
# hyprlang if !NONEXISTENT_VAR_2
testStringColon = ee:ee:ee
# hyprlang endif
# hyprlang if NONEXISTENT_VAR
testStringColon = ee:ee:ee:22
# hyprlang endif
# hyprlang endif
# hyprlang noerror true
errorVariable = true
# hyprlang noerror false
# hyprlang if NONEXISTENT_VAR
customType = bcd
# hyprlang endif
# hyprlang if MY_VAR
categoryKeyword = oops, this one shouldn't call the handler, not fun
testUseKeyword = yes
# hyprlang endif
testCategory {
testValueInt = 123456
testValueHex = 0xF
@ -80,16 +122,43 @@ specialGeneric {
specialAnonymous {
value = 2
testHandlerDontOverride = true
}
specialAnonymous {
value = 3
}
specialAnonymousNested {
nested:value1 = 1
nested:value2 = 2
nested1:nested2:value1 = 10
nested1:nested2:value2 = 11
}
specialAnonymousNested {
nested {
value1 = 3
value2 = 4
}
nested1 {
nested2 {
value1 = 12
value2 = 13
}
}
}
flagsStuff {
value = 2
}
multiline = \
very \
long \
command
testCategory:testValueHex = 0xFFfFaAbB
$RECURSIVE1 = a
@ -104,3 +173,13 @@ flagsabc = test
#doSomethingFunny = 1, 2, 3, 4 # Funnier!
#testSpaces = abc , def # many spaces, should be trimmed
sameKeywordSpecialCat = pablo
sameKeywordSpecialCat:two:hola = rose
sameKeywordSpecialCat {
one {
some_size = 44
some_radius = 7.6
}
}

View file

@ -0,0 +1,20 @@
# Careful when modifying this file. Line numbers are part of the test.
multiline = \
one \
two \
three
# Line numbers reported in errors should match the actual line numbers of the source file
# even after multi-line configs. Any errors reported should use the line number of the
# first line of any multi-line config.
this \
should \
cause \
error \
on \
line \
12
# A config file cannot end with a bashslash because we are expecting another line! Even in a comment! \

View file

@ -11,7 +11,7 @@ std::string garbage() {
std::string chars;
for (int i = 0; i < len; ++i) {
chars += rand() % 254 + 1;
chars += std::to_string((rand() % 254) + 1);
}
return chars;

173
tests/parse/main.cpp Executable file → Normal file
View file

@ -23,12 +23,14 @@ namespace Colors {
}
// globals for testing
bool barrelRoll = false;
std::string flagsFound = "";
Hyprlang::CConfig* pConfig = nullptr;
std::string currentPath = "";
std::string ignoreKeyword = "";
std::string useKeyword = "";
bool barrelRoll = false;
std::string flagsFound = "";
Hyprlang::CConfig* pConfig = nullptr;
std::string currentPath = "";
std::string ignoreKeyword = "";
std::string useKeyword = "";
std::string sameKeywordSpecialCat = "";
bool testHandlerDontOverrideValue = false;
static std::vector<std::string> categoryKeywordActualValues;
static Hyprlang::CParseResult handleDoABarrelRoll(const char* COMMAND, const char* VALUE) {
@ -48,7 +50,7 @@ static Hyprlang::CParseResult handleFlagsTest(const char* COMMAND, const char* V
}
static Hyprlang::CParseResult handleCategoryKeyword(const char* COMMAND, const char* VALUE) {
categoryKeywordActualValues.push_back(VALUE);
categoryKeywordActualValues.emplace_back(VALUE);
return Hyprlang::CParseResult();
}
@ -74,6 +76,19 @@ static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALU
return pConfig->parseFile(PATH.c_str());
}
static Hyprlang::CParseResult handleSameKeywordSpecialCat(const char* COMMAND, const char* VALUE) {
sameKeywordSpecialCat = VALUE;
return Hyprlang::CParseResult();
}
static Hyprlang::CParseResult handleTestHandlerDontOverride(const char* COMMAND, const char* VALUE) {
testHandlerDontOverrideValue = true;
Hyprlang::CParseResult result;
return result;
}
static Hyprlang::CParseResult handleCustomValueSet(const char* VALUE, void** data) {
if (!*data)
*data = calloc(1, sizeof(int64_t));
@ -99,6 +114,8 @@ int main(int argc, char** argv, char** envp) {
if (!getenv("SHELL"))
setenv("SHELL", "/bin/sh", true);
setenv("TEST_ENV", "1", true);
std::cout << "Starting test\n";
Hyprlang::CConfig config("./config/config.conf", {});
@ -107,11 +124,21 @@ int main(int argc, char** argv, char** envp) {
// setup config
config.addConfigValue("testInt", (Hyprlang::INT)0);
config.addConfigValue("testExpr", (Hyprlang::INT)0);
config.addConfigValue("testEscapedExpr", "");
config.addConfigValue("testEscapedExpr2", "");
config.addConfigValue("testEscapedExpr3", "");
config.addConfigValue("testEscapedEscape", "");
config.addConfigValue("testMixedEscapedExpression", "");
config.addConfigValue("testMixedEscapedExpression2", "");
config.addConfigValue("testImbeddedEscapedExpression", "");
config.addConfigValue("testDynamicEscapedExpression", "");
config.addConfigValue("testFloat", 0.F);
config.addConfigValue("testVec", Hyprlang::SVector2D{69, 420});
config.addConfigValue("testVec", Hyprlang::SVector2D{.x = 69, .y = 420});
config.addConfigValue("testString", "");
config.addConfigValue("testStringColon", "");
config.addConfigValue("testEnv", "");
config.addConfigValue("testEnv2", "");
config.addConfigValue("testVar", (Hyprlang::INT)0);
config.addConfigValue("categoryKeyword", (Hyprlang::STRING) "");
config.addConfigValue("testStringQuotes", "");
@ -131,25 +158,44 @@ int main(int argc, char** argv, char** envp) {
config.addConfigValue("myColors:random", (Hyprlang::INT)0);
config.addConfigValue("customType", {Hyprlang::CConfigCustomValueType{&handleCustomValueSet, &handleCustomValueDestroy, "def"}});
config.registerHandler(&handleDoABarrelRoll, "doABarrelRoll", {false});
config.registerHandler(&handleFlagsTest, "flags", {true});
config.registerHandler(&handleSource, "source", {false});
config.registerHandler(&handleTestIgnoreKeyword, "testIgnoreKeyword", {false});
config.registerHandler(&handleTestUseKeyword, ":testUseKeyword", {false});
config.registerHandler(&handleNoop, "testCategory:testUseKeyword", {false});
config.registerHandler(&handleCategoryKeyword, "testCategory:categoryKeyword", {false});
config.registerHandler(&handleDoABarrelRoll, "doABarrelRoll", {.allowFlags = false});
config.registerHandler(&handleFlagsTest, "flags", {.allowFlags = true});
config.registerHandler(&handleSource, "source", {.allowFlags = false});
config.registerHandler(&handleTestIgnoreKeyword, "testIgnoreKeyword", {.allowFlags = false});
config.registerHandler(&handleTestUseKeyword, ":testUseKeyword", {.allowFlags = false});
config.registerHandler(&handleNoop, "testCategory:testUseKeyword", {.allowFlags = false});
config.registerHandler(&handleCategoryKeyword, "testCategory:categoryKeyword", {.allowFlags = false});
config.registerHandler(&handleTestHandlerDontOverride, "testHandlerDontOverride", {.allowFlags = false});
config.addSpecialCategory("special", {"key"});
config.addSpecialCategory("special", {.key = "key"});
config.addSpecialConfigValue("special", "value", (Hyprlang::INT)0);
config.addSpecialCategory("specialAnonymous", {nullptr, false, true});
config.addSpecialCategory("specialAnonymous", {.key = nullptr, .ignoreMissing = false, .anonymousKeyBased = true});
config.addSpecialConfigValue("specialAnonymous", "value", (Hyprlang::INT)0);
config.addSpecialConfigValue("specialAnonymous", "testHandlerDontOverride", (Hyprlang::INT)0);
config.addSpecialCategory("specialAnonymousNested", {.key = nullptr, .ignoreMissing = false, .anonymousKeyBased = true});
config.addSpecialConfigValue("specialAnonymousNested", "nested:value1", (Hyprlang::INT)0);
config.addSpecialConfigValue("specialAnonymousNested", "nested:value2", (Hyprlang::INT)0);
config.addSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value1", (Hyprlang::INT)0);
config.addSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value2", (Hyprlang::INT)0);
config.addConfigValue("multiline", "");
config.registerHandler(&handleSameKeywordSpecialCat, "sameKeywordSpecialCat", {.allowFlags = false});
config.addSpecialCategory("sameKeywordSpecialCat", {.key = nullptr, .ignoreMissing = true, .anonymousKeyBased = false});
config.commence();
config.addSpecialCategory("specialGeneric:one", {nullptr, true});
config.addSpecialCategory("sameKeywordSpecialCat:one", {.key = nullptr, .ignoreMissing = true});
config.addSpecialConfigValue("sameKeywordSpecialCat:one", "some_size", (Hyprlang::INT)10);
config.addSpecialConfigValue("sameKeywordSpecialCat:one", "some_radius", (Hyprlang::FLOAT)0.0);
config.addSpecialCategory("sameKeywordSpecialCat:two", {.key = nullptr, .ignoreMissing = true});
config.addSpecialConfigValue("sameKeywordSpecialCat:two", "hola", "");
config.addSpecialCategory("specialGeneric:one", {.key = nullptr, .ignoreMissing = true});
config.addSpecialConfigValue("specialGeneric:one", "value", (Hyprlang::INT)0);
config.addSpecialCategory("specialGeneric:two", {nullptr, true});
config.addSpecialCategory("specialGeneric:two", {.key = nullptr, .ignoreMissing = true});
config.addSpecialConfigValue("specialGeneric:two", "value", (Hyprlang::INT)0);
const Hyprlang::CConfigValue copyTest = {(Hyprlang::INT)1};
@ -167,7 +213,7 @@ int main(int argc, char** argv, char** envp) {
std::cout << " → Testing values\n";
EXPECT(std::any_cast<int64_t>(config.getConfigValue("testInt")), 123);
EXPECT(std::any_cast<float>(config.getConfigValue("testFloat")), 123.456f);
auto EXP = Hyprlang::SVector2D{69, 420};
auto EXP = Hyprlang::SVector2D{.x = 69, .y = 420};
EXPECT(std::any_cast<Hyprlang::SVector2D>(config.getConfigValue("testVec")), EXP);
EXPECT(std::any_cast<const char*>(config.getConfigValue("testString")), std::string{"Hello World! # This is not a comment!"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testStringQuotes")), std::string{"\"Hello World!\""});
@ -185,6 +231,38 @@ int main(int argc, char** argv, char** envp) {
EXPECT(ignoreKeyword, "aaa");
EXPECT(useKeyword, "yes");
// test special category with same name as a keyword
EXPECT(sameKeywordSpecialCat, std::string_view{"pablo"});
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("sameKeywordSpecialCat:one", "some_size")), (Hyprlang::INT)44);
EXPECT(std::any_cast<float>(config.getSpecialConfigValue("sameKeywordSpecialCat:one", "some_radius")), (Hyprlang::FLOAT)7.6);
EXPECT(std::any_cast<const char*>(config.getSpecialConfigValue("sameKeywordSpecialCat:two", "hola")), std::string_view{"rose"});
// Test templated wrapper
auto T1 = Hyprlang::CSimpleConfigValue<Hyprlang::INT>(&config, "testInt");
auto T2 = Hyprlang::CSimpleConfigValue<Hyprlang::FLOAT>(&config, "testFloat");
auto T3 = Hyprlang::CSimpleConfigValue<Hyprlang::SVector2D>(&config, "testVec");
auto T4 = Hyprlang::CSimpleConfigValue<std::string>(&config, "testString");
EXPECT(*T1, 123);
EXPECT(*T2, 123.456F);
EXPECT(*T3, EXP);
EXPECT(*T4, "Hello World! # This is not a comment!");
// test expressions
std::cout << " → Testing expressions\n";
EXPECT(std::any_cast<int64_t>(config.getConfigValue("testExpr")), 1335);
// test expression escape
std::cout << " → Testing expression escapes\n";
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEscapedExpr")), std::string{"{{testInt + 7}}"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEscapedExpr2")), std::string{"{{testInt + 7}}"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEscapedExpr3")), std::string{"{{3 + 8}}"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEscapedEscape")), std::string{"\\5"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testMixedEscapedExpression")), std::string{"-2 {{ {{50 + 50}} / {{10 * 5}} }}"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testMixedEscapedExpression2")), std::string{"{{8\\13}} should equal \"{{8\\13}}\""});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testImbeddedEscapedExpression")), std::string{"{{10 + 10}}"});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testDynamicEscapedExpression")), std::string{"{{ moved: 500 expr: {{1000 / 2}} }}"});
// test static values
std::cout << " → Testing static values\n";
static auto* const PTESTINT = config.getConfigValuePtr("testInt")->getDataStaticPtr();
@ -196,6 +274,7 @@ int main(int argc, char** argv, char** envp) {
std::cout << " → Testing handlers\n";
EXPECT(barrelRoll, true);
EXPECT(flagsFound, std::string{"abc"});
EXPECT(testHandlerDontOverrideValue, false);
EXPECT(categoryKeywordActualValues.at(0), "we are having fun");
EXPECT(categoryKeywordActualValues.at(1), "so much fun");
@ -231,9 +310,18 @@ int main(int argc, char** argv, char** envp) {
EXPECT(config.parseDynamic("$RECURSIVE1 = d").error, false);
EXPECT(std::any_cast<const char*>(config.getConfigValue("testStringRecursive")), std::string{"dbc"});
// test expression escape with dynamic vars
EXPECT(config.parseDynamic("$MOVING_VAR = 500").error, false);
EXPECT(std::any_cast<const char*>(config.getConfigValue("testDynamicEscapedExpression")), std::string{"{{ moved: 250 expr: {{500 / 2}} }}"});
// test dynamic exprs
EXPECT(config.parseDynamic("testExpr = {{EXPR_VAR * 2}}").error, false);
EXPECT(std::any_cast<int64_t>(config.getConfigValue("testExpr")), 1339L * 2);
// test env variables
std::cout << " → Testing env variables\n";
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEnv")), std::string{getenv("SHELL")});
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEnv2")), std::string{"1"});
// test special categories
std::cout << " → Testing special categories\n";
@ -243,6 +331,17 @@ int main(int argc, char** argv, char** envp) {
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialGeneric:two", "value")), 2);
EXPECT(config.parseDynamic("special[b]:value = 3").error, false);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("special", "value", "b")), 3);
EXPECT(config.parseDynamic("specialAnonymousNested[c]:nested:value1 = 4").error, false);
EXPECT(config.parseDynamic("specialAnonymousNested[c]:nested:value2 = 5").error, false);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value1", "c")), (Hyprlang::INT)4);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", "c")), (Hyprlang::INT)5);
EXPECT(config.parseDynamic("specialAnonymousNested[c]:nested:value2 = 6").error, false);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", "c")), (Hyprlang::INT)6);
EXPECT(config.parseDynamic("special[a]:value = 69").error, false);
EXPECT(config.parseDynamic("special[b]:value = 420").error, false);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("special", "value", "a")), 69);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("special", "value", "b")), 420);
// test dynamic special variable
EXPECT(config.parseDynamic("$SPECIALVAL1 = 2").error, false);
@ -257,8 +356,22 @@ int main(int argc, char** argv, char** envp) {
// test anonymous
EXPECT(config.listKeysForSpecialCategory("specialAnonymous").size(), 2);
const auto KEYS = config.listKeysForSpecialCategory("specialAnonymous");
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymous", "value", KEYS[0].c_str())), 2);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymous", "testHandlerDontOverride", KEYS[0].c_str())), 1);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymous", "value", KEYS[1].c_str())), 3);
// test anonymous nested
EXPECT(config.listKeysForSpecialCategory("specialAnonymousNested").size(), 2 + /*from dynamic*/ 1);
const auto KEYS2 = config.listKeysForSpecialCategory("specialAnonymousNested");
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value1", KEYS2[0].c_str())), 1);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", KEYS2[0].c_str())), 2);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value1", KEYS2[1].c_str())), 3);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", KEYS2[1].c_str())), 4);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value1", KEYS2[0].c_str())), 10);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value2", KEYS2[0].c_str())), 11);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value1", KEYS2[1].c_str())), 12);
EXPECT(std::any_cast<int64_t>(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value2", KEYS2[1].c_str())), 13);
// test sourcing
std::cout << " → Testing sourcing\n";
EXPECT(std::any_cast<int64_t>(config.getConfigValue("myColors:pink")), (Hyprlang::INT)0xFFc800c8);
@ -269,6 +382,15 @@ int main(int argc, char** argv, char** envp) {
std::cout << " → Testing custom types\n";
EXPECT(*reinterpret_cast<int64_t*>(std::any_cast<void*>(config.getConfigValue("customType"))), (Hyprlang::INT)1);
// test multiline config
EXPECT(std::any_cast<const char*>(config.getConfigValue("multiline")), std::string{"very long command"});
// test dynamic env
setenv("TEST_ENV", "2", true);
config.parse();
std::cout << " → Testing dynamic env variables\n";
EXPECT(std::any_cast<const char*>(config.getConfigValue("testEnv2")), std::string{"2"});
std::cout << " → Testing error.conf\n";
Hyprlang::CConfig errorConfig("./config/error.conf", {.verifyOnly = true, .throwAllErrors = true});
@ -297,6 +419,17 @@ int main(int argc, char** argv, char** envp) {
EXPECT(ERRORS2.error, true);
const auto ERRORSTR2 = std::string{ERRORS2.getError()};
EXPECT(std::count(ERRORSTR2.begin(), ERRORSTR2.end(), '\n'), 9 - 1);
Hyprlang::CConfig multilineErrorConfig("./config/multiline-errors.conf", {.verifyOnly = true, .throwAllErrors = true});
multilineErrorConfig.commence();
const auto ERRORS3 = multilineErrorConfig.parse();
EXPECT(ERRORS3.error, true);
const auto ERRORSTR3 = std::string{ERRORS3.getError()};
// Error on line 12
EXPECT(ERRORSTR3.contains("12"), true);
// Backslash at end of file
EXPECT(ERRORSTR3.contains("backslash"), true);
} catch (const char* e) {
std::cout << Colors::RED << "Error: " << Colors::RESET << e << "\n";
return 1;