├── VERSION ├── src ├── core.hpp ├── logger.hpp ├── public.hpp ├── config.hpp ├── common.cpp └── config.cpp ├── COPYRIGHT ├── tests ├── config │ ├── error.conf │ ├── colors.conf │ ├── invalid-numbers.conf │ ├── multiline-errors.conf │ └── config.conf ├── fuzz │ └── main.cpp └── parse │ └── main.cpp ├── .gitignore ├── hyprlang.pc.in ├── nix └── default.nix ├── README.md ├── flake.lock ├── flake.nix ├── .github └── workflows │ ├── nix.yml │ └── arch.yml ├── .clang-format ├── CMakeLists.txt ├── .clang-tidy ├── LICENSE └── include └── hyprlang.hpp /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.7 2 | -------------------------------------------------------------------------------- /src/core.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | -------------------------------------------------------------------------------- /src/logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace Logger {} -------------------------------------------------------------------------------- /src/public.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../include/hyprlang.hpp" -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright: Copyright (C) 2023-2024 Hypr Development 2 | License: LGPL-3.0-Only -------------------------------------------------------------------------------- /tests/config/error.conf: -------------------------------------------------------------------------------- 1 | # This config houses two errors 2 | 3 | someValue = 123 4 | 5 | category { 6 | invalid_line -------------------------------------------------------------------------------- /tests/config/colors.conf: -------------------------------------------------------------------------------- 1 | 2 | myColors { 3 | pink = rgba(200, 0, 200, 1.0) 4 | green = rgb(20, 240, 20) 5 | random = 0xFFFF$MY_VAR 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user 2 | CMakeCache.txt 3 | CMakeFiles 4 | CMakeScripts 5 | Testing 6 | Makefile 7 | cmake_install.cmake 8 | install_manifest.txt 9 | compile_commands.json 10 | CTestTestfile.cmake 11 | _deps 12 | .vscode 13 | build/ 14 | doxygen/ 15 | doxygen-awesome-css/ 16 | .cache/ -------------------------------------------------------------------------------- /hyprlang.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@PREFIX@ 2 | includedir=@INCLUDE@ 3 | libdir=@LIBDIR@ 4 | 5 | Name: hyprlang 6 | URL: https://github.com/hyprwm/hyprlang 7 | Description: The official implementation library for the hypr config language. 8 | Version: @HYPRLANG_VERSION@ 9 | Cflags: -I${includedir} 10 | Libs: -L${libdir} -lhyprlang 11 | -------------------------------------------------------------------------------- /tests/config/invalid-numbers.conf: -------------------------------------------------------------------------------- 1 | # Every number/color in this file is invalid 2 | 3 | invalidHex = 0x1Q 4 | emptyHex = 0x 5 | hugeHex = 0xFFFFFFFFFFFFFFFF 6 | 7 | invalidInt = 1A 8 | emptyInt = 9 | 10 | invalidColor = rgb(ABCDEQ) 11 | invalidFirstCharColor = rgb(QABCDE) 12 | 13 | invalidColorAlpha = rgba(9ABCDEFQ) 14 | invalidFirstCharColorAlpha = rgba(Q9ABCDEF) 15 | 16 | -------------------------------------------------------------------------------- /tests/config/multiline-errors.conf: -------------------------------------------------------------------------------- 1 | # Careful when modifying this file. Line numbers are part of the test. 2 | 3 | multiline = \ 4 | one \ 5 | two \ 6 | three 7 | 8 | # Line numbers reported in errors should match the actual line numbers of the source file 9 | # even after multi-line configs. Any errors reported should use the line number of the 10 | # first line of any multi-line config. 11 | 12 | this \ 13 | should \ 14 | cause \ 15 | error \ 16 | on \ 17 | line \ 18 | 12 19 | 20 | # A config file cannot end with a bashslash because we are expecting another line! Even in a comment! \ 21 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | cmake, 5 | hyprutils, 6 | pkg-config, 7 | version ? "git", 8 | doCheck ? false, 9 | }: 10 | stdenv.mkDerivation { 11 | pname = "hyprlang"; 12 | inherit version doCheck; 13 | src = ../.; 14 | 15 | nativeBuildInputs = [ 16 | cmake 17 | pkg-config 18 | ]; 19 | 20 | buildInputs = [hyprutils]; 21 | 22 | outputs = ["out" "dev"]; 23 | 24 | meta = with lib; { 25 | homepage = "https://github.com/hyprwm/hyprlang"; 26 | description = "The official implementation library for the hypr config language"; 27 | license = licenses.lgpl3Only; 28 | platforms = platforms.linux; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /tests/fuzz/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #define FUZZ_ITERS 1337 6 | 7 | std::string garbage() { 8 | srand(time(nullptr)); 9 | 10 | int len = rand() % 10000; 11 | 12 | std::string chars; 13 | for (int i = 0; i < len; ++i) { 14 | chars += std::to_string((rand() % 254) + 1); 15 | } 16 | 17 | return chars; 18 | } 19 | 20 | int main(int argc, char** argv, char** envp) { 21 | 22 | Hyprlang::CConfig config("./eeeeeeeUnused", {.allowMissingConfig = true}); 23 | config.addConfigValue("test", {(Hyprlang::INT)0}); 24 | 25 | config.parseDynamic(""); 26 | config.parseDynamic("", ""); 27 | config.parseDynamic("}"); 28 | for (size_t i = 0; i < FUZZ_ITERS; ++i) { 29 | config.parseDynamic(garbage().c_str(), garbage().c_str()); 30 | config.parseDynamic((garbage() + "=" + garbage()).c_str()); 31 | config.parseDynamic(garbage().c_str()); 32 | config.parseDynamic((garbage() + " {").c_str()); 33 | config.parseDynamic((std::string{"test = "} + garbage()).c_str()); 34 | config.parseDynamic((std::string{"$"} + garbage()).c_str()); 35 | config.parseDynamic((std::string{"$VAR = "} + garbage()).c_str()); 36 | } 37 | config.parseDynamic("}"); 38 | 39 | std::cout << "Success, no fuzzing errors\n"; 40 | 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyprlang 2 | 3 | The hypr configuration language is an extremely efficient, yet easy to work with, configuration language 4 | for linux applications. 5 | 6 | It's user-friendly, easy to grasp, and easy to implement. 7 | 8 | ## Building and installation 9 | 10 | Building is done via CMake: 11 | ```sh 12 | cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build 13 | cmake --build ./build --config Release --target hyprlang -j`nproc 2>/dev/null || getconf _NPROCESSORS_CONF` 14 | ``` 15 | Install with: 16 | ```sh 17 | sudo cmake --install ./build 18 | ``` 19 | 20 | ## Example config 21 | 22 | ```ini 23 | bakery { 24 | counter_color = rgba(ee22eeff) # color by rgba() 25 | door_color = rgba(122, 176, 91, 0.1) # color by rgba() 26 | dimensions = 10 20 # vec2 27 | employees = 3 # int 28 | average_time_spent = 8.13 # float 29 | hackers_password = 0xDEADBEEF # int, as hex 30 | 31 | # nested categories 32 | secrets { 33 | password = hyprland # string 34 | } 35 | } 36 | 37 | # variable 38 | $NUM_ORDERS = 3 39 | 40 | cakes { 41 | number = $NUM_ORDERS # use a variable 42 | colors = red, green, blue # string 43 | } 44 | 45 | # keywords, invoke your own handler with the parameters 46 | add_baker = Jeremy, 26, Warsaw 47 | add_baker = Andrew, 21, Berlin 48 | add_baker = Koichi, 18, Morioh 49 | ``` 50 | 51 | ## Docs 52 | 53 | Visit [wiki.hypr.land/Hypr-Ecosystem/hyprlang/](https://wiki.hypr.land/Hypr-Ecosystem/hyprlang/) to see the documentation. 54 | 55 | ### Example implementation 56 | 57 | For an example implementation, take a look at the `tests/` directory. 58 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "hyprutils": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "systems": [ 9 | "systems" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1749135356, 14 | "narHash": "sha256-Q8mAKMDsFbCEuq7zoSlcTuxgbIBVhfIYpX0RjE32PS0=", 15 | "owner": "hyprwm", 16 | "repo": "hyprutils", 17 | "rev": "e36db00dfb3a3d3fdcc4069cb292ff60d2699ccb", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "hyprwm", 22 | "repo": "hyprutils", 23 | "type": "github" 24 | } 25 | }, 26 | "nixpkgs": { 27 | "locked": { 28 | "lastModified": 1748929857, 29 | "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", 30 | "owner": "NixOS", 31 | "repo": "nixpkgs", 32 | "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "NixOS", 37 | "ref": "nixos-unstable", 38 | "repo": "nixpkgs", 39 | "type": "github" 40 | } 41 | }, 42 | "root": { 43 | "inputs": { 44 | "hyprutils": "hyprutils", 45 | "nixpkgs": "nixpkgs", 46 | "systems": "systems" 47 | } 48 | }, 49 | "systems": { 50 | "locked": { 51 | "lastModified": 1689347949, 52 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 53 | "owner": "nix-systems", 54 | "repo": "default-linux", 55 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "nix-systems", 60 | "repo": "default-linux", 61 | "type": "github" 62 | } 63 | } 64 | }, 65 | "root": "root", 66 | "version": 7 67 | } 68 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "The official implementation library for the hypr config language"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | systems.url = "github:nix-systems/default-linux"; 7 | 8 | hyprutils = { 9 | url = "github:hyprwm/hyprutils"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | inputs.systems.follows = "systems"; 12 | }; 13 | }; 14 | 15 | outputs = { 16 | self, 17 | nixpkgs, 18 | systems, 19 | ... 20 | } @ inputs: let 21 | inherit (nixpkgs) lib; 22 | eachSystem = lib.genAttrs (import systems); 23 | pkgsFor = eachSystem (system: 24 | import nixpkgs { 25 | localSystem.system = system; 26 | overlays = with self.overlays; [hyprlang]; 27 | }); 28 | mkDate = longDate: (lib.concatStringsSep "-" [ 29 | (builtins.substring 0 4 longDate) 30 | (builtins.substring 4 2 longDate) 31 | (builtins.substring 6 2 longDate) 32 | ]); 33 | 34 | version = lib.removeSuffix "\n" (builtins.readFile ./VERSION); 35 | in { 36 | overlays = { 37 | default = self.overlays.hyprlang; 38 | hyprlang = lib.composeManyExtensions [ 39 | inputs.hyprutils.overlays.default 40 | (final: prev: { 41 | hyprlang = final.callPackage ./nix/default.nix { 42 | stdenv = final.gcc15Stdenv; 43 | version = version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); 44 | }; 45 | hyprlang-with-tests = final.hyprlang.override {doCheck = true;}; 46 | }) 47 | ]; 48 | }; 49 | 50 | packages = eachSystem (system: { 51 | default = self.packages.${system}.hyprlang; 52 | inherit (pkgsFor.${system}) hyprlang hyprlang-with-tests; 53 | }); 54 | 55 | formatter = eachSystem (system: pkgsFor.${system}.alejandra); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | jobs: 5 | nix: 6 | strategy: 7 | matrix: 8 | package: 9 | - hyprlang 10 | - hyprlang-with-tests 11 | 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install Nix 17 | uses: nixbuild/nix-quick-install-action@v31 18 | with: 19 | nix_conf: | 20 | keep-env-derivations = true 21 | keep-outputs = true 22 | 23 | - name: Restore and save Nix store 24 | uses: nix-community/cache-nix-action@v6 25 | with: 26 | # restore and save a cache using this key 27 | primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} 28 | # if there's no cache hit, restore a cache by this prefix 29 | restore-prefixes-first-match: nix-${{ runner.os }}- 30 | # collect garbage until the Nix store size (in bytes) is at most this number 31 | # before trying to save a new cache 32 | # 1G = 1073741824 33 | gc-max-store-size-linux: 1G 34 | # do purge caches 35 | purge: true 36 | # purge all versions of the cache 37 | purge-prefixes: nix-${{ runner.os }}- 38 | # created more than this number of seconds ago 39 | purge-created: 0 40 | # or, last accessed more than this number of seconds ago 41 | # relative to the start of the `Post Restore and save Nix store` phase 42 | purge-last-accessed: 0 43 | # except any version with the key that is the same as the `primary-key` 44 | purge-primary-key: never 45 | 46 | # not needed (yet) 47 | # - uses: cachix/cachix-action@v12 48 | # with: 49 | # name: hyprland 50 | # authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 51 | 52 | - name: Build & Test 53 | run: nix build .#${{ matrix.package }} --print-build-logs 54 | 55 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | 5 | AccessModifierOffset: -2 6 | AlignAfterOpenBracket: Align 7 | AlignConsecutiveMacros: true 8 | AlignConsecutiveAssignments: true 9 | AlignEscapedNewlines: Right 10 | AlignOperands: false 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: true 16 | AllowShortCaseLabelsOnASingleLine: true 17 | AllowShortFunctionsOnASingleLine: Empty 18 | AllowShortIfStatementsOnASingleLine: Never 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BreakBeforeBraces: Attach 26 | BreakBeforeTernaryOperators: false 27 | BreakConstructorInitializers: AfterColon 28 | ColumnLimit: 180 29 | CompactNamespaces: false 30 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 31 | ExperimentalAutoDetectBinPacking: false 32 | FixNamespaceComments: false 33 | IncludeBlocks: Preserve 34 | IndentCaseLabels: true 35 | IndentWidth: 4 36 | PointerAlignment: Left 37 | ReflowComments: false 38 | SortIncludes: false 39 | SortUsingDeclarations: false 40 | SpaceAfterCStyleCast: false 41 | SpaceAfterLogicalNot: false 42 | SpaceAfterTemplateKeyword: true 43 | SpaceBeforeCtorInitializerColon: true 44 | SpaceBeforeInheritanceColon: true 45 | SpaceBeforeParens: ControlStatements 46 | SpaceBeforeRangeBasedForLoopColon: true 47 | SpaceInEmptyParentheses: false 48 | SpacesBeforeTrailingComments: 1 49 | SpacesInAngles: false 50 | SpacesInCStyleCastParentheses: false 51 | SpacesInContainerLiterals: false 52 | SpacesInParentheses: false 53 | SpacesInSquareBrackets: false 54 | Standard: Auto 55 | TabWidth: 4 56 | UseTab: Never 57 | 58 | AllowShortEnumsOnASingleLine: false 59 | 60 | BraceWrapping: 61 | AfterEnum: false 62 | 63 | AlignConsecutiveDeclarations: AcrossEmptyLines 64 | 65 | NamespaceIndentation: All 66 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19) 2 | 3 | file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) 4 | string(STRIP ${VER_RAW} HYPRLANG_VERSION) 5 | 6 | project( 7 | hyprlang 8 | VERSION ${HYPRLANG_VERSION} 9 | DESCRIPTION "A library to parse hypr config files") 10 | 11 | include(CTest) 12 | include(GNUInstallDirs) 13 | 14 | set(PREFIX ${CMAKE_INSTALL_PREFIX}) 15 | set(INCLUDE ${CMAKE_INSTALL_FULL_INCLUDEDIR}) 16 | set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR}) 17 | 18 | configure_file(hyprlang.pc.in hyprlang.pc @ONLY) 19 | 20 | if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) 21 | message(STATUS "Configuring hyprlang in Debug") 22 | add_compile_definitions(HYPRLAND_DEBUG) 23 | else() 24 | add_compile_options(-O3) 25 | message(STATUS "Configuring hyprlang in Release") 26 | endif() 27 | 28 | add_compile_definitions(HYPRLANG_INTERNAL) 29 | 30 | set(CMAKE_CXX_STANDARD 23) 31 | add_compile_options( 32 | -Wall 33 | -Wextra 34 | -Wpedantic 35 | -Wno-unused-parameter 36 | -Wno-missing-field-initializers) 37 | set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) 38 | 39 | find_package(PkgConfig REQUIRED) 40 | pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprutils>=0.7.1) 41 | 42 | file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/hyprlang.hpp") 43 | 44 | add_library(hyprlang SHARED ${SRCFILES}) 45 | target_include_directories( 46 | hyprlang 47 | PUBLIC "./include" 48 | PRIVATE "./src") 49 | set_target_properties( 50 | hyprlang 51 | PROPERTIES VERSION ${HYPRLANG_VERSION} 52 | SOVERSION 2 53 | PUBLIC_HEADER include/hyprlang.hpp) 54 | 55 | target_link_libraries(hyprlang PkgConfig::deps) 56 | 57 | add_library(hypr::hyprlang ALIAS hyprlang) 58 | install(TARGETS hyprlang) 59 | 60 | # tests 61 | add_custom_target(tests) 62 | 63 | add_executable(hyprlang_test "tests/parse/main.cpp") 64 | target_link_libraries(hyprlang_test PRIVATE hypr::hyprlang) 65 | add_test( 66 | NAME "Parsing" 67 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests 68 | COMMAND hyprlang_test "parse") 69 | add_dependencies(tests hyprlang_test) 70 | 71 | add_executable(hyprlang_fuzz "tests/fuzz/main.cpp") 72 | target_link_libraries(hyprlang_fuzz PRIVATE hypr::hyprlang) 73 | add_test( 74 | NAME "Fuzz" 75 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests 76 | COMMAND hyprlang_fuzz "fuzz") 77 | add_dependencies(tests hyprlang_fuzz) 78 | 79 | # Installation 80 | install( 81 | TARGETS hyprlang 82 | PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 83 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) 84 | install(FILES ${CMAKE_BINARY_DIR}/hyprlang.pc 85 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) 86 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | WarningsAsErrors: '*' 2 | HeaderFilterRegex: '.*\.hpp' 3 | FormatStyle: 'file' 4 | Checks: > 5 | -*, 6 | bugprone-*, 7 | -bugprone-easily-swappable-parameters, 8 | -bugprone-forward-declaration-namespace, 9 | -bugprone-forward-declaration-namespace, 10 | -bugprone-macro-parentheses, 11 | -bugprone-narrowing-conversions, 12 | -bugprone-branch-clone, 13 | -bugprone-assignment-in-if-condition, 14 | concurrency-*, 15 | -concurrency-mt-unsafe, 16 | cppcoreguidelines-*, 17 | -cppcoreguidelines-owning-memory, 18 | -cppcoreguidelines-avoid-magic-numbers, 19 | -cppcoreguidelines-pro-bounds-constant-array-index, 20 | -cppcoreguidelines-avoid-const-or-ref-data-members, 21 | -cppcoreguidelines-non-private-member-variables-in-classes, 22 | -cppcoreguidelines-avoid-goto, 23 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, 24 | -cppcoreguidelines-avoid-do-while, 25 | -cppcoreguidelines-avoid-non-const-global-variables, 26 | -cppcoreguidelines-special-member-functions, 27 | -cppcoreguidelines-explicit-virtual-functions, 28 | -cppcoreguidelines-avoid-c-arrays, 29 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 30 | -cppcoreguidelines-narrowing-conversions, 31 | -cppcoreguidelines-pro-type-union-access, 32 | -cppcoreguidelines-pro-type-member-init, 33 | -cppcoreguidelines-macro-usage, 34 | -cppcoreguidelines-macro-to-enum, 35 | -cppcoreguidelines-init-variables, 36 | -cppcoreguidelines-pro-type-cstyle-cast, 37 | -cppcoreguidelines-pro-type-vararg, 38 | -cppcoreguidelines-pro-type-reinterpret-cast, 39 | google-global-names-in-headers, 40 | -google-readability-casting, 41 | google-runtime-operator, 42 | misc-*, 43 | -misc-unused-parameters, 44 | -misc-no-recursion, 45 | -misc-non-private-member-variables-in-classes, 46 | -misc-include-cleaner, 47 | -misc-use-anonymous-namespace, 48 | -misc-const-correctness, 49 | modernize-*, 50 | -modernize-return-braced-init-list, 51 | -modernize-use-trailing-return-type, 52 | -modernize-use-using, 53 | -modernize-use-override, 54 | -modernize-avoid-c-arrays, 55 | -modernize-macro-to-enum, 56 | -modernize-loop-convert, 57 | -modernize-use-nodiscard, 58 | -modernize-pass-by-value, 59 | -modernize-use-auto, 60 | performance-*, 61 | -performance-avoid-endl, 62 | -performance-unnecessary-value-param, 63 | portability-std-allocator-const, 64 | readability-*, 65 | -readability-function-cognitive-complexity, 66 | -readability-function-size, 67 | -readability-identifier-length, 68 | -readability-magic-numbers, 69 | -readability-uppercase-literal-suffix, 70 | -readability-braces-around-statements, 71 | -readability-redundant-access-specifiers, 72 | -readability-else-after-return, 73 | -readability-container-data-pointer, 74 | -readability-implicit-bool-conversion, 75 | -readability-avoid-nested-conditional-operator, 76 | -readability-redundant-member-init, 77 | -readability-redundant-string-init, 78 | -readability-avoid-const-params-in-decls, 79 | -readability-named-parameter, 80 | -readability-convert-member-functions-to-static, 81 | -readability-qualified-auto, 82 | -readability-make-member-function-const, 83 | -readability-isolate-declaration, 84 | -readability-inconsistent-declaration-parameter-name, 85 | -clang-diagnostic-error, 86 | 87 | CheckOptions: 88 | performance-for-range-copy.WarnOnAllAutoCopies: true 89 | performance-inefficient-string-concatenation.StrictMode: true 90 | readability-braces-around-statements.ShortStatementLines: 0 91 | readability-identifier-naming.ClassCase: CamelCase 92 | readability-identifier-naming.ClassIgnoredRegexp: I.* 93 | readability-identifier-naming.ClassPrefix: C # We can't use regex here?!?!?!? 94 | readability-identifier-naming.EnumCase: CamelCase 95 | readability-identifier-naming.EnumPrefix: e 96 | readability-identifier-naming.EnumConstantCase: UPPER_CASE 97 | readability-identifier-naming.FunctionCase: camelBack 98 | readability-identifier-naming.NamespaceCase: CamelCase 99 | readability-identifier-naming.NamespacePrefix: N 100 | readability-identifier-naming.StructPrefix: S 101 | readability-identifier-naming.StructCase: CamelCase 102 | -------------------------------------------------------------------------------- /tests/config/config.conf: -------------------------------------------------------------------------------- 1 | 2 | # Test comment 3 | ## This is also a comment 4 | ## This is a comment with space as a first character 5 | ## This is a comment with tab as a first character 6 | ## This is a comment with leading spaces and tabs 7 | ##### Comment with more hash tags 8 | 9 | testInt = 123 10 | testFloat = 123.456 11 | testString = Hello World! ## This is not a comment! # This is! 12 | 13 | $MY_VAR = 1337 14 | $MY_VAR_2 = $MY_VAR 15 | testVar = $MY_VAR$MY_VAR_2 16 | 17 | $EXPR_VAR = {{MY_VAR + 2}} 18 | testExpr = {{EXPR_VAR - 4}} 19 | 20 | testEscapedExpr = \{{testInt + 7}} 21 | testEscapedExpr2 = {\{testInt + 7}} 22 | testEscapedExpr3 = \{\{3 + 8}} 23 | testEscapedEscape = \\{{10 - 5}} 24 | testMixedEscapedExpression = {{8 - 10}} \{{ \{{50 + 50}} / \{{10 * 5}} }} 25 | testMixedEscapedExpression2 = {\{8\\{{10 + 3}}}} should equal "\{{8\13}}" 26 | 27 | $ESCAPED_TEXT = \{{10 + 10}} 28 | testImbeddedEscapedExpression = $ESCAPED_TEXT 29 | 30 | $MOVING_VAR = 1000 31 | $DYNAMIC_EXPRESSION = moved: {{$MOVING_VAR / 2}} expr: \{{$MOVING_VAR / 2}} 32 | testDynamicEscapedExpression = \{{ $DYNAMIC_EXPRESSION }} 33 | 34 | testEnv = $SHELL 35 | testEnv2 = $TEST_ENV 36 | 37 | source = ./colors.conf 38 | 39 | customType = abc 40 | 41 | # hyprlang if !NONEXISTENT_VAR 42 | 43 | # hyprlang if !NONEXISTENT_VAR_2 44 | 45 | testStringColon = ee:ee:ee 46 | 47 | # hyprlang endif 48 | 49 | # hyprlang if NONEXISTENT_VAR 50 | 51 | testStringColon = ee:ee:ee:22 52 | 53 | # hyprlang endif 54 | 55 | # hyprlang endif 56 | 57 | # hyprlang noerror true 58 | 59 | errorVariable = true 60 | 61 | # hyprlang noerror false 62 | 63 | # hyprlang if NONEXISTENT_VAR 64 | 65 | customType = bcd 66 | 67 | # hyprlang endif 68 | 69 | # hyprlang if MY_VAR 70 | 71 | categoryKeyword = oops, this one shouldn't call the handler, not fun 72 | testUseKeyword = yes 73 | 74 | # hyprlang endif 75 | 76 | testCategory { 77 | testValueInt = 123456 78 | testValueHex = 0xF 79 | 80 | testColor1 = rgb(255, 255, 255) 81 | testColor2 = rgba(0, 0, 0, 1.0) 82 | testColor3 = rgba(ffeeff22) 83 | 84 | testIgnoreKeyword = aaa 85 | testUseKeyword = no 86 | 87 | nested1 { 88 | testValueNest = 1 89 | nested2 { 90 | testValueNest = 1 91 | } 92 | categoryKeyword = this one should not either 93 | } 94 | 95 | categoryKeyword = we are having fun 96 | categoryKeyword = so much fun 97 | categoryKeyword = im the fun one at parties 98 | } 99 | 100 | $SPECIALVAL1 = 1 101 | 102 | special { 103 | key = a 104 | value = $SPECIALVAL1 105 | } 106 | 107 | special[b] { 108 | value = 2 109 | } 110 | 111 | specialGeneric { 112 | one { 113 | value = 1 114 | copyTest = 2 115 | } 116 | 117 | two { 118 | value = 2 119 | nonexistent = abc 120 | } 121 | } 122 | 123 | specialAnonymous { 124 | value = 2 125 | testHandlerDontOverride = true 126 | } 127 | 128 | specialAnonymous { 129 | value = 3 130 | } 131 | 132 | specialAnonymousNested { 133 | nested:value1 = 1 134 | nested:value2 = 2 135 | nested1:nested2:value1 = 10 136 | nested1:nested2:value2 = 11 137 | } 138 | 139 | specialAnonymousNested { 140 | nested { 141 | value1 = 3 142 | value2 = 4 143 | } 144 | 145 | nested1 { 146 | nested2 { 147 | value1 = 12 148 | value2 = 13 149 | } 150 | } 151 | } 152 | 153 | flagsStuff { 154 | value = 2 155 | } 156 | 157 | multiline = \ 158 | very \ 159 | long \ 160 | command 161 | 162 | testCategory:testValueHex = 0xFFfFaAbB 163 | 164 | $RECURSIVE1 = a 165 | $RECURSIVE2 = $RECURSIVE1b 166 | testStringRecursive = $RECURSIVE2c 167 | 168 | testStringQuotes = "Hello World!" 169 | #testDefault = 123 170 | 171 | doABarrelRoll = woohoo, some, params # Funny! 172 | flagsabc = test 173 | #doSomethingFunny = 1, 2, 3, 4 # Funnier! 174 | #testSpaces = abc , def # many spaces, should be trimmed 175 | 176 | sameKeywordSpecialCat = pablo 177 | 178 | sameKeywordSpecialCat:two:hola = rose 179 | 180 | sameKeywordSpecialCat { 181 | one { 182 | some_size = 44 183 | some_radius = 7.6 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | #include "public.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct SHandler { 10 | std::string name = ""; 11 | Hyprlang::SHandlerOptions options; 12 | Hyprlang::PCONFIGHANDLERFUNC func = nullptr; 13 | }; 14 | 15 | struct SVariable { 16 | std::string name = ""; 17 | std::string value = ""; 18 | 19 | struct SVarLine { 20 | std::string line; 21 | std::vector categories; 22 | SSpecialCategory* specialCategory = nullptr; // if applicable 23 | }; 24 | 25 | std::vector linesContainingVar; // for dynamic updates 26 | 27 | bool truthy() { 28 | return value.length() > 0; 29 | } 30 | }; 31 | 32 | // remember to also edit CConfigValue if editing 33 | enum eDataType { 34 | CONFIGDATATYPE_EMPTY, 35 | CONFIGDATATYPE_INT, 36 | CONFIGDATATYPE_FLOAT, 37 | CONFIGDATATYPE_STR, 38 | CONFIGDATATYPE_VEC2, 39 | CONFIGDATATYPE_CUSTOM, 40 | }; 41 | 42 | // CUSTOM is stored as STR!! 43 | struct SConfigDefaultValue { 44 | std::any data; 45 | eDataType type = CONFIGDATATYPE_EMPTY; 46 | 47 | // this sucks but I have no better idea 48 | Hyprlang::PCONFIGCUSTOMVALUEHANDLERFUNC handler = nullptr; 49 | Hyprlang::PCONFIGCUSTOMVALUEDESTRUCTOR dtor = nullptr; 50 | }; 51 | 52 | struct SSpecialCategoryDescriptor { 53 | std::string name = ""; 54 | std::string key = ""; 55 | std::unordered_map defaultValues; 56 | bool dontErrorOnMissing = false; 57 | bool anonymous = false; 58 | }; 59 | 60 | struct SSpecialCategory { 61 | SSpecialCategoryDescriptor* descriptor = nullptr; 62 | std::string name = ""; 63 | std::string key = ""; // empty means no key 64 | std::unordered_map values; 65 | bool isStatic = false; 66 | 67 | void applyDefaults(); 68 | 69 | // for easy anonymous ID'ing 70 | size_t anonymousID = 0; 71 | }; 72 | 73 | enum eGetNextLineFailure : uint8_t { 74 | GETNEXTLINEFAILURE_EOF = 0, 75 | GETNEXTLINEFAILURE_BACKSLASH, 76 | }; 77 | 78 | class CConfigImpl { 79 | public: 80 | std::string path = ""; 81 | std::string originalPath = ""; 82 | 83 | // if not-empty, used instead of path 84 | std::string rawConfigString = ""; 85 | 86 | std::unordered_map values; 87 | std::unordered_map defaultValues; 88 | std::vector handlers; 89 | std::vector variables; 90 | std::vector envVariables; 91 | std::vector> specialCategories; 92 | std::vector> specialCategoryDescriptors; 93 | 94 | std::vector categories; 95 | std::string currentSpecialKey = ""; 96 | SSpecialCategory* currentSpecialCategory = nullptr; // if applicable 97 | 98 | std::string parseError = ""; 99 | 100 | Hyprlang::SConfigOptions configOptions; 101 | 102 | std::optional parseComment(const std::string& comment); 103 | std::expected parseExpression(const std::string& s); 104 | SVariable* getVariable(const std::string& name); 105 | void recheckEnv(); 106 | 107 | struct SIfBlockData { 108 | bool failed = false; 109 | }; 110 | 111 | struct { 112 | bool noError = false; 113 | 114 | std::vector ifDatas; 115 | } currentFlags; 116 | }; 117 | -------------------------------------------------------------------------------- /src/common.cpp: -------------------------------------------------------------------------------- 1 | #include "public.hpp" 2 | #include "config.hpp" 3 | #include 4 | 5 | using namespace Hyprlang; 6 | 7 | void CParseResult::setError(const std::string& err) { 8 | error = true; 9 | errorStdString = err; 10 | errorString = errorStdString.c_str(); 11 | } 12 | 13 | void CParseResult::setError(const char* err) { 14 | error = true; 15 | errorStdString = err; 16 | errorString = errorStdString.c_str(); 17 | } 18 | 19 | CConfigValue::~CConfigValue() { 20 | if (m_pData) { 21 | switch (m_eType) { 22 | case CONFIGDATATYPE_INT: delete (int64_t*)m_pData; break; 23 | case CONFIGDATATYPE_FLOAT: delete (float*)m_pData; break; 24 | case CONFIGDATATYPE_VEC2: delete (SVector2D*)m_pData; break; 25 | case CONFIGDATATYPE_CUSTOM: delete (CConfigCustomValueType*)m_pData; break; 26 | case CONFIGDATATYPE_STR: delete[] (char*)m_pData; break; 27 | 28 | default: break; // oh no? 29 | } 30 | } 31 | } 32 | 33 | CConfigValue::CConfigValue(const int64_t value) : m_eType(CONFIGDATATYPE_INT), m_pData(new int64_t) { 34 | *reinterpret_cast(m_pData) = value; 35 | } 36 | 37 | CConfigValue::CConfigValue(const float value) : m_eType(CONFIGDATATYPE_FLOAT), m_pData(new float) { 38 | *reinterpret_cast(m_pData) = value; 39 | } 40 | 41 | CConfigValue::CConfigValue(const SVector2D value) : m_eType(CONFIGDATATYPE_VEC2), m_pData(new SVector2D) { 42 | *reinterpret_cast(m_pData) = value; 43 | } 44 | 45 | CConfigValue::CConfigValue(const char* value) : m_eType(CONFIGDATATYPE_STR), m_pData(new char[strlen(value) + 1]) { 46 | strncpy((char*)m_pData, value, strlen(value)); 47 | ((char*)m_pData)[strlen(value)] = '\0'; 48 | } 49 | 50 | CConfigValue::CConfigValue(CConfigCustomValueType&& value) : m_eType(CONFIGDATATYPE_CUSTOM), m_pData(new CConfigCustomValueType(value)) { 51 | ; 52 | } 53 | 54 | CConfigValue::CConfigValue(const CConfigValue& other) : m_eType(other.m_eType) { 55 | setFrom(&other); 56 | } 57 | 58 | CConfigValue::CConfigValue() { 59 | ; 60 | } 61 | 62 | void* CConfigValue::dataPtr() const { 63 | return m_pData; 64 | } 65 | 66 | void* const* CConfigValue::getDataStaticPtr() const { 67 | return &m_pData; 68 | } 69 | 70 | CConfigCustomValueType::CConfigCustomValueType(PCONFIGCUSTOMVALUEHANDLERFUNC handler_, PCONFIGCUSTOMVALUEDESTRUCTOR dtor_, const char* def) : 71 | handler(handler_), dtor(dtor_), defaultVal(def), lastVal(def) { 72 | ; 73 | } 74 | 75 | CConfigCustomValueType::~CConfigCustomValueType() { 76 | dtor(&data); 77 | } 78 | 79 | void CConfigValue::defaultFrom(SConfigDefaultValue& ref) { 80 | m_eType = (CConfigValue::eDataType)ref.type; 81 | switch (m_eType) { 82 | case CONFIGDATATYPE_FLOAT: { 83 | if (!m_pData) 84 | m_pData = new float; 85 | *reinterpret_cast(m_pData) = std::any_cast(ref.data); 86 | break; 87 | } 88 | case CONFIGDATATYPE_INT: { 89 | if (!m_pData) 90 | m_pData = new int64_t; 91 | *reinterpret_cast(m_pData) = std::any_cast(ref.data); 92 | break; 93 | } 94 | case CONFIGDATATYPE_STR: { 95 | if (m_pData) 96 | delete[] (char*)m_pData; 97 | std::string str = std::any_cast(ref.data); 98 | m_pData = new char[str.length() + 1]; 99 | strncpy((char*)m_pData, str.c_str(), str.length()); // TODO: please just wrap this 100 | ((char*)m_pData)[str.length()] = '\0'; 101 | break; 102 | } 103 | case CONFIGDATATYPE_VEC2: { 104 | if (!m_pData) 105 | m_pData = new SVector2D; 106 | *reinterpret_cast(m_pData) = std::any_cast(ref.data); 107 | break; 108 | } 109 | case CONFIGDATATYPE_CUSTOM: { 110 | if (!m_pData) 111 | m_pData = new CConfigCustomValueType(ref.handler, ref.dtor, std::any_cast(ref.data).c_str()); 112 | CConfigCustomValueType* type = reinterpret_cast(m_pData); 113 | type->handler(std::any_cast(ref.data).c_str(), &type->data); 114 | type->lastVal = std::any_cast(ref.data); 115 | break; 116 | } 117 | default: { 118 | throw "bad defaultFrom type"; 119 | } 120 | } 121 | 122 | m_bSetByUser = false; 123 | } 124 | 125 | void CConfigValue::setFrom(const CConfigValue* const ref) { 126 | switch (m_eType) { 127 | case CONFIGDATATYPE_FLOAT: { 128 | if (!m_pData) 129 | m_pData = new float; 130 | *reinterpret_cast(m_pData) = std::any_cast(ref->getValue()); 131 | break; 132 | } 133 | case CONFIGDATATYPE_INT: { 134 | if (!m_pData) 135 | m_pData = new int64_t; 136 | *reinterpret_cast(m_pData) = std::any_cast(ref->getValue()); 137 | break; 138 | } 139 | case CONFIGDATATYPE_STR: { 140 | if (m_pData) 141 | delete[] (char*)m_pData; 142 | std::string str = std::any_cast(ref->getValue()); 143 | m_pData = new char[str.length() + 1]; 144 | strncpy((char*)m_pData, str.c_str(), str.length()); 145 | ((char*)m_pData)[str.length()] = '\0'; 146 | break; 147 | } 148 | case CONFIGDATATYPE_VEC2: { 149 | if (!m_pData) 150 | m_pData = new SVector2D; 151 | *reinterpret_cast(m_pData) = std::any_cast(ref->getValue()); 152 | break; 153 | } 154 | case CONFIGDATATYPE_CUSTOM: { 155 | CConfigCustomValueType* reftype = reinterpret_cast(ref->m_pData); 156 | 157 | if (!m_pData) 158 | m_pData = new CConfigCustomValueType(reftype->handler, reftype->dtor, reftype->defaultVal.c_str()); 159 | 160 | CConfigCustomValueType* type = reinterpret_cast(m_pData); 161 | type->handler(reftype->lastVal.c_str(), &type->data); 162 | break; 163 | } 164 | default: { 165 | throw "bad defaultFrom type"; 166 | } 167 | } 168 | } 169 | 170 | void CConfigValue::setFrom(std::any ref) { 171 | switch (m_eType) { 172 | case CONFIGDATATYPE_FLOAT: { 173 | if (!m_pData) 174 | m_pData = new float; 175 | *reinterpret_cast(m_pData) = std::any_cast(ref); 176 | break; 177 | } 178 | case CONFIGDATATYPE_INT: { 179 | if (!m_pData) 180 | m_pData = new int64_t; 181 | *reinterpret_cast(m_pData) = std::any_cast(ref); 182 | break; 183 | } 184 | case CONFIGDATATYPE_STR: { 185 | if (m_pData) 186 | delete[] (char*)m_pData; 187 | std::string str = std::any_cast(ref); 188 | m_pData = new char[str.length() + 1]; 189 | strncpy((char*)m_pData, str.c_str(), str.length()); 190 | ((char*)m_pData)[str.length()] = '\0'; 191 | break; 192 | } 193 | case CONFIGDATATYPE_VEC2: { 194 | if (!m_pData) 195 | m_pData = new SVector2D; 196 | *reinterpret_cast(m_pData) = std::any_cast(ref); 197 | break; 198 | } 199 | case CONFIGDATATYPE_CUSTOM: { 200 | throw "bad defaultFrom type (cannot custom from std::any)"; 201 | break; 202 | } 203 | default: { 204 | throw "bad defaultFrom type"; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /.github/workflows/arch.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test (Arch) 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | jobs: 5 | asan: 6 | name: "gcc build / ASan tests" 7 | runs-on: ubuntu-latest 8 | container: 9 | image: archlinux 10 | steps: 11 | - name: Checkout repository actions 12 | uses: actions/checkout@v4 13 | with: 14 | sparse-checkout: .github/actions 15 | 16 | - name: Get required pkgs 17 | run: | 18 | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf 19 | pacman --noconfirm --noprogressbar -Syyu 20 | pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang git pixman gtest 21 | 22 | - name: Get hyprutils-git 23 | run: | 24 | git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build 25 | 26 | - name: Build with gcc 27 | run: | 28 | CXXFLAGS="-fsanitize=address" CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build 29 | CXXFLAGS="-fsanitize=address" CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` 30 | cmake --install ./build 31 | 32 | - name: Run tests 33 | run: | 34 | cd ./build && ctest --output-on-failure 35 | 36 | ubsan: 37 | name: "gcc build / UBSan tests" 38 | runs-on: ubuntu-latest 39 | container: 40 | image: archlinux 41 | steps: 42 | - name: Checkout repository actions 43 | uses: actions/checkout@v4 44 | with: 45 | sparse-checkout: .github/actions 46 | 47 | - name: Get required pkgs 48 | run: | 49 | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf 50 | pacman --noconfirm --noprogressbar -Syyu 51 | pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang git pixman gtest 52 | 53 | - name: Get hyprutils-git 54 | run: | 55 | git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build 56 | 57 | - name: Build with gcc 58 | run: | 59 | CXXFLAGS="-fsanitize=undefined" CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build 60 | CXXFLAGS="-fsanitize=undefined" CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` 61 | cmake --install ./build 62 | 63 | - name: Run tests 64 | run: | 65 | cd ./build && ctest --output-on-failure 66 | 67 | msan: 68 | name: "gcc build / MSan tests" 69 | runs-on: ubuntu-latest 70 | container: 71 | image: archlinux 72 | steps: 73 | - name: Checkout repository actions 74 | uses: actions/checkout@v4 75 | with: 76 | sparse-checkout: .github/actions 77 | 78 | - name: Get required pkgs 79 | run: | 80 | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf 81 | pacman --noconfirm --noprogressbar -Syyu 82 | pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang git pixman gtest 83 | 84 | - name: Get hyprutils-git 85 | run: | 86 | git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build 87 | 88 | - name: Build with gcc 89 | run: | 90 | CXXFLAGS="-fsanitize=leak" CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build 91 | CXXFLAGS="-fsanitize=leak" CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` 92 | cmake --install ./build 93 | 94 | - name: Run tests 95 | run: | 96 | cd ./build && ctest --output-on-failure 97 | 98 | clang: 99 | name: "clang build / gcc test" 100 | runs-on: ubuntu-latest 101 | container: 102 | image: archlinux 103 | steps: 104 | - name: Checkout repository actions 105 | uses: actions/checkout@v4 106 | with: 107 | sparse-checkout: .github/actions 108 | 109 | - name: Get required pkgs 110 | run: | 111 | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf 112 | pacman --noconfirm --noprogressbar -Syyu 113 | pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ git pixman gtest 114 | 115 | - name: Get hyprutils-git 116 | run: | 117 | git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build 118 | 119 | - name: Build hyprlang with clang 120 | run: | 121 | CC="/usr/bin/clang" CXX="/usr/bin/clang++" cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_CXX_FLAGS="-stdlib=libc++" -S . -B ./build 122 | CC="/usr/bin/clang" CXX="/usr/bin/clang++" cmake --build ./build --config Release --target hyprlang -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` 123 | cmake --install ./build 124 | 125 | - name: Build tests with gcc 126 | run: | 127 | rm -rf ./build 128 | CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build 129 | CC="/usr/bin/gcc" CXX="/usr/bin/g++" cmake --build ./build --config Release --target tests -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` 130 | 131 | - name: Run tests 132 | run: | 133 | cd ./build && ctest --output-on-failure 134 | 135 | doxygen: 136 | name: "Deploy docs" 137 | runs-on: ubuntu-latest 138 | if: github.ref == 'refs/heads/main' 139 | container: 140 | image: archlinux 141 | steps: 142 | - name: Checkout repository actions 143 | uses: actions/checkout@v4 144 | with: 145 | sparse-checkout: .github/actions 146 | 147 | - name: Get required pkgs 148 | run: | 149 | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf 150 | pacman --noconfirm --noprogressbar -Syyu 151 | pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang doxygen git openssh 152 | 153 | - name: Get doxygen theme 154 | run: | 155 | git clone https://github.com/jothepro/doxygen-awesome-css 156 | cd doxygen-awesome-css && git checkout v2.3.1 && cd .. 157 | 158 | - name: Build doxygen site 159 | run: | 160 | doxygen hyprlang-docs 161 | 162 | - name: Deploy 163 | env: 164 | PK: ${{ secrets.UPDATE_DOCS_PK }} 165 | PT: ${{ secrets.DEPLOY_PORT5 }} 166 | AD: ${{ secrets.DEPLOY_USER }} 167 | PH: ${{ secrets.DEPLOY_PATH }} 168 | run: | 169 | echo "$PK" > ./pk 170 | chmod 400 ./pk 171 | eval $(ssh-agent -s) && ssh-add ./pk 2>&1 > /dev/null 172 | scp -O -o "StrictHostKeyChecking=no" -q -o "LogLevel=QUIET" -P $PT -r ./doxygen/html/* $AD:.$PH 2>&1 > /dev/null 173 | 174 | 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /include/hyprlang.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef HYPRLANG_HPP 4 | #define HYPRLANG_HPP 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class CConfigImpl; 15 | struct SConfigDefaultValue; 16 | struct SSpecialCategory; 17 | 18 | #define HYPRLANG_END_MAGIC 0x1337BEEF 19 | 20 | namespace Hyprlang { 21 | 22 | struct SVector2D; 23 | class CConfigCustomValueType; 24 | 25 | /* Variable typedefs */ 26 | 27 | /*! 28 | Basic integer config type 29 | */ 30 | typedef int64_t INT; 31 | 32 | /*! 33 | Basic float config type 34 | */ 35 | typedef float FLOAT; 36 | 37 | /*! 38 | Basic string config type 39 | */ 40 | typedef const char* STRING; 41 | 42 | /*! 43 | Basic vec2 config type 44 | */ 45 | typedef SVector2D VEC2; 46 | 47 | /*! 48 | Custom config type 49 | */ 50 | typedef CConfigCustomValueType CUSTOMTYPE; 51 | 52 | /*! 53 | A very simple vector type 54 | */ 55 | struct SVector2D { 56 | float x = 0, y = 0; 57 | 58 | // 59 | bool operator==(const SVector2D& rhs) const { 60 | return x == rhs.x && y == rhs.y; 61 | } 62 | 63 | friend std::ostream& operator<<(std::ostream& os, const SVector2D& rhs) { 64 | return os << "[" << rhs.x << ", " << rhs.y << "]"; 65 | } 66 | }; 67 | 68 | class CParseResult { 69 | public: 70 | bool error = false; 71 | /*! 72 | Get this ParseResult's error string. 73 | Pointer valid until the error string is changed or this 74 | object gets destroyed. 75 | */ 76 | const char* getError() const { 77 | return errorString; 78 | } 79 | /*! 80 | Set an error contained by this ParseResult. 81 | Creates a copy of the string, does not take ownership. 82 | */ 83 | void setError(const char* err); 84 | 85 | private: 86 | void setError(const std::string& err); 87 | 88 | std::string errorStdString = ""; 89 | const char* errorString = nullptr; 90 | 91 | friend class CConfig; 92 | }; 93 | 94 | /*! 95 | Generic struct for options for the config parser 96 | */ 97 | struct SConfigOptions { 98 | /*! 99 | Don't throw errors on missing values. 100 | */ 101 | int verifyOnly = false; 102 | 103 | /*! 104 | Return all errors instead of just the first 105 | */ 106 | int throwAllErrors = false; 107 | 108 | /*! 109 | \since 0.2.0 110 | 111 | Don't throw on a missing config file. Carry on as if nothing happened. 112 | */ 113 | int allowMissingConfig = false; 114 | 115 | /*! 116 | \since 0.4.2 117 | 118 | Treat configPath as a raw config stream. 119 | */ 120 | int pathIsStream = false; 121 | 122 | // INTERNAL: DO NOT MODIFY 123 | int __internal_struct_end = HYPRLANG_END_MAGIC; 124 | }; 125 | 126 | /*! 127 | Generic struct for options for handlers 128 | */ 129 | struct SHandlerOptions { 130 | /*! 131 | Allow flags for this handler 132 | */ 133 | bool allowFlags = false; 134 | 135 | // INTERNAL: DO NOT MODIFY 136 | int __internal_struct_end = HYPRLANG_END_MAGIC; 137 | }; 138 | 139 | /*! 140 | Generic struct for options for special categories 141 | */ 142 | struct SSpecialCategoryOptions { 143 | /*! 144 | a key is the name of a value that will be the identifier of a special category 145 | can be left null for no key, aka a generic one 146 | keys are always strings. Default key value is "0" 147 | */ 148 | const char* key = nullptr; 149 | 150 | /*! 151 | don't pop up an error if the config value is missing 152 | */ 153 | int ignoreMissing = false; 154 | 155 | /*! 156 | Make this category an anonymous special one. 157 | key has to be nullptr. 158 | 159 | Anonymous special categories behave like key-based ones, but the keys 160 | will be automatically assigned without user input. 161 | 162 | \since 0.4.0 163 | */ 164 | int anonymousKeyBased = false; 165 | 166 | // INTERNAL: DO NOT MODIFY 167 | int __internal_struct_end = HYPRLANG_END_MAGIC; 168 | }; 169 | 170 | /*! 171 | typedefs 172 | */ 173 | typedef CParseResult (*PCONFIGHANDLERFUNC)(const char* COMMAND, const char* VALUE); 174 | typedef CParseResult (*PCONFIGCUSTOMVALUEHANDLERFUNC)(const char* VALUE, void** data); 175 | typedef void (*PCONFIGCUSTOMVALUEDESTRUCTOR)(void** data); 176 | 177 | /*! 178 | Container for a custom config value type 179 | When creating, pass your handler. 180 | Handler will receive a void** that points to a void* that you can set to your own 181 | thing. Pass a dtor to free whatever you allocated when the custom value type is being released. 182 | data may always be pointing to a nullptr. 183 | */ 184 | class CConfigCustomValueType { 185 | public: 186 | CConfigCustomValueType(PCONFIGCUSTOMVALUEHANDLERFUNC handler_, PCONFIGCUSTOMVALUEDESTRUCTOR dtor_, const char* defaultValue); 187 | ~CConfigCustomValueType(); 188 | 189 | /*! 190 | \since 0.3.0 191 | 192 | get the data pointer for the custom value type. 193 | */ 194 | void* getData() { 195 | return data; 196 | } 197 | 198 | private: 199 | PCONFIGCUSTOMVALUEHANDLERFUNC handler = nullptr; 200 | PCONFIGCUSTOMVALUEDESTRUCTOR dtor = nullptr; 201 | void* data = nullptr; 202 | std::string defaultVal = ""; 203 | std::string lastVal = ""; 204 | 205 | friend class CConfigValue; 206 | friend class CConfig; 207 | }; 208 | 209 | /*! 210 | Container for a config value 211 | */ 212 | class CConfigValue { 213 | public: 214 | CConfigValue(); 215 | CConfigValue(const INT value); 216 | CConfigValue(const FLOAT value); 217 | CConfigValue(const STRING value); 218 | CConfigValue(const VEC2 value); 219 | CConfigValue(CUSTOMTYPE&& value); 220 | CConfigValue(CConfigValue&&) = delete; 221 | CConfigValue(const CConfigValue&&) = delete; 222 | CConfigValue(CConfigValue&) = delete; 223 | 224 | /*! 225 | \since 0.3.0 226 | */ 227 | CConfigValue(const CConfigValue&); 228 | 229 | ~CConfigValue(); 230 | 231 | /*! 232 | Return a pointer to the data. Prefer getDataStaticPtr() 233 | */ 234 | void* dataPtr() const; 235 | 236 | /*! 237 | \since 0.2.0 238 | 239 | Return a static pointer to the m_pData. 240 | As long as this configValue is alive, this pointer is valid. 241 | CConfigValues are alive as long as the owning CConfig is alive. 242 | 243 | Please note only the first (outer) pointer is static. The second 244 | may (and most likely will) be changing. 245 | 246 | For all types except STRING typeof(**retval) is the config value type 247 | (e.g. INT or FLOAT) 248 | 249 | Please note STRING is a special type and instead of 250 | typeof(**retval) being const char*, typeof(\*retval) is a const char*. 251 | */ 252 | void* const* getDataStaticPtr() const; 253 | 254 | /*! 255 | Get the contained value as an std::any. 256 | For strings, this is a const char*. 257 | For custom data types, this is a void* representing the data ptr stored by it. 258 | */ 259 | std::any getValue() const { 260 | switch (m_eType) { 261 | case CONFIGDATATYPE_EMPTY: throw; 262 | case CONFIGDATATYPE_INT: return std::any(*reinterpret_cast(m_pData)); 263 | case CONFIGDATATYPE_FLOAT: return std::any(*reinterpret_cast(m_pData)); 264 | case CONFIGDATATYPE_STR: return std::any(reinterpret_cast(m_pData)); 265 | case CONFIGDATATYPE_VEC2: return std::any(*reinterpret_cast(m_pData)); 266 | case CONFIGDATATYPE_CUSTOM: return std::any(reinterpret_cast(m_pData)->data); 267 | default: throw; 268 | } 269 | return {}; // unreachable 270 | } 271 | 272 | /*! 273 | \since 0.3.0 274 | 275 | a flag to notify whether this value has been set explicitly by the user, 276 | or not. 277 | */ 278 | bool m_bSetByUser = false; 279 | 280 | private: 281 | // remember to also edit config.hpp if editing 282 | enum eDataType { 283 | CONFIGDATATYPE_EMPTY, 284 | CONFIGDATATYPE_INT, 285 | CONFIGDATATYPE_FLOAT, 286 | CONFIGDATATYPE_STR, 287 | CONFIGDATATYPE_VEC2, 288 | CONFIGDATATYPE_CUSTOM, 289 | }; 290 | eDataType m_eType = eDataType::CONFIGDATATYPE_EMPTY; 291 | void* m_pData = nullptr; 292 | void defaultFrom(SConfigDefaultValue& ref); 293 | void setFrom(std::any ref); 294 | void setFrom(const CConfigValue* const ref); 295 | 296 | friend class CConfig; 297 | }; 298 | 299 | /*! 300 | Base class for a config file 301 | */ 302 | class CConfig { 303 | public: 304 | CConfig(const char* configPath, const SConfigOptions& options); 305 | ~CConfig(); 306 | 307 | /*! 308 | Add a config value, for example myCategory:myValue. 309 | This has to be done before commence() 310 | Value provided becomes default. 311 | */ 312 | void addConfigValue(const char* name, const CConfigValue& value); 313 | 314 | /*! 315 | Register a handler. Can be called anytime, though not recommended 316 | to do this dynamically . 317 | */ 318 | void registerHandler(PCONFIGHANDLERFUNC func, const char* name, SHandlerOptions options); 319 | 320 | /*! 321 | \since 0.3.0 322 | 323 | Unregister a handler. 324 | */ 325 | void unregisterHandler(const char* name); 326 | 327 | /*! 328 | Commence the config state. Config becomes immutable, as in 329 | no new values may be added or removed. Required for parsing. 330 | */ 331 | void commence(); 332 | 333 | /*! 334 | Add a special category. Can be done dynamically. 335 | */ 336 | void addSpecialCategory(const char* name, SSpecialCategoryOptions options); 337 | 338 | /*! 339 | \since 0.3.0 340 | 341 | Remove a special category. Can be done dynamically. 342 | */ 343 | void removeSpecialCategory(const char* name); 344 | 345 | /*! 346 | \since 0.3.2 347 | 348 | Add a config value to a special category. 349 | 350 | \note Before 0.3.2, this takes a `const CConfigValue` (non-ref) 351 | */ 352 | void addSpecialConfigValue(const char* cat, const char* name, const CConfigValue& value); 353 | 354 | /*! 355 | Remove a config value from a special category. 356 | */ 357 | void removeSpecialConfigValue(const char* cat, const char* name); 358 | 359 | /*! 360 | Parse the config. Refresh the values. 361 | */ 362 | CParseResult parse(); 363 | 364 | /*! 365 | Same as parse(), but parse a specific file, without any refreshing. 366 | recommended to use for stuff like source = path.conf 367 | */ 368 | CParseResult parseFile(const char* file); 369 | 370 | /*! 371 | Parse a single "line", dynamically. 372 | Values set by this are temporary and will be overwritten 373 | by default / config on the next parse() 374 | */ 375 | CParseResult parseDynamic(const char* line); 376 | CParseResult parseDynamic(const char* command, const char* value); 377 | 378 | /*! 379 | Get a config's value ptr. These are static. 380 | nullptr on fail 381 | */ 382 | CConfigValue* getConfigValuePtr(const char* name); 383 | 384 | /*! 385 | Get a special category's config value ptr. These are only static for static (key-less) 386 | categories. 387 | key can be nullptr for static categories. Cannot be nullptr for id-based categories. 388 | nullptr on fail. 389 | */ 390 | CConfigValue* getSpecialConfigValuePtr(const char* category, const char* name, const char* key = nullptr); 391 | 392 | /*! 393 | Get a config value's stored value. Empty on fail 394 | */ 395 | std::any getConfigValue(const char* name) { 396 | CConfigValue* val = getConfigValuePtr(name); 397 | if (!val) 398 | return {}; 399 | return val->getValue(); 400 | } 401 | 402 | /*! 403 | Get a special config value's stored value. Empty on fail. 404 | */ 405 | std::any getSpecialConfigValue(const char* category, const char* name, const char* key = nullptr) { 406 | CConfigValue* val = getSpecialConfigValuePtr(category, name, key); 407 | if (!val) 408 | return {}; 409 | return val->getValue(); 410 | } 411 | 412 | /*! 413 | Check whether a special category with the provided key value exists 414 | 415 | \since 0.3.0 416 | */ 417 | bool specialCategoryExistsForKey(const char* category, const char* key); 418 | 419 | /*! 420 | Get a vector with all registered keys for a special category 421 | 422 | It's an error to query this for a static or non-existent category 423 | 424 | \since 0.4.0 425 | */ 426 | std::vector listKeysForSpecialCategory(const char* category) { 427 | const char** cats = nullptr; 428 | size_t len = 0; 429 | retrieveKeysForCat(category, &cats, &len); 430 | 431 | if (len == 0) 432 | return {}; 433 | 434 | std::vector result; 435 | for (size_t i = 0; i < len; ++i) { 436 | result.push_back(cats[i]); 437 | } 438 | 439 | free(cats); 440 | 441 | return result; 442 | } 443 | 444 | /*! 445 | Change the root path of the config 446 | 447 | \since 0.6.7 448 | */ 449 | void changeRootPath(const char* path); 450 | 451 | private: 452 | bool m_bCommenced = false; 453 | 454 | CConfigImpl* impl; 455 | 456 | CParseResult parseLine(std::string line, bool dynamic = false); 457 | std::pair configSetValueSafe(const std::string& command, const std::string& value); 458 | CParseResult parseVariable(const std::string& lhs, const std::string& rhs, bool dynamic = false); 459 | void clearState(); 460 | void applyDefaultsToCat(SSpecialCategory& cat); 461 | void retrieveKeysForCat(const char* category, const char*** out, size_t* len); 462 | CParseResult parseRawStream(const std::string& stream); 463 | }; 464 | 465 | /*! 466 | Templated wrapper for Hyprlang values. Much more straightforward to use. 467 | 468 | \since 0.6.0 469 | */ 470 | template 471 | class CSimpleConfigValue { 472 | public: 473 | CSimpleConfigValue(CConfig* const pConfig, const char* val) { 474 | const auto VAL = pConfig->getConfigValuePtr(val); 475 | 476 | if (!VAL) { 477 | std::println("CSimpleConfigValue: value not found"); 478 | abort(); 479 | } 480 | 481 | // NOLINTNEXTLINE 482 | p_ = VAL->getDataStaticPtr(); 483 | 484 | #ifdef HYPRLAND_DEBUG 485 | // verify type 486 | const auto ANY = VAL->getValue(); 487 | const auto TYPE = std::type_index(ANY.type()); 488 | 489 | // exceptions 490 | const bool STRINGEX = (typeid(T) == typeid(std::string) && TYPE == typeid(Hyprlang::STRING)); 491 | const bool CUSTOMEX = (typeid(T) == typeid(Hyprlang::CUSTOMTYPE) && (TYPE == typeid(Hyprlang::CUSTOMTYPE*) || TYPE == typeid(void*) /* dunno why it does this? */)); 492 | 493 | if (typeid(T) != TYPE && !STRINGEX && !CUSTOMEX) { 494 | std::println("CSimpleConfigValue: Mismatched type in CConfigValue, got {} but has {}", typeid(T).name(), TYPE.name()); 495 | abort(); 496 | } 497 | #endif 498 | } 499 | 500 | T* ptr() const { 501 | return *(T* const*)p_; 502 | } 503 | 504 | T operator*() const { 505 | return *ptr(); 506 | } 507 | 508 | private: 509 | void* const* p_ = nullptr; 510 | }; 511 | 512 | template <> 513 | inline std::string* CSimpleConfigValue::ptr() const { 514 | std::print("Impossible to implement ptr() of CConfigValue"); 515 | abort(); 516 | return nullptr; 517 | } 518 | 519 | template <> 520 | inline std::string CSimpleConfigValue::operator*() const { 521 | return std::string{*(Hyprlang::STRING*)p_}; 522 | } 523 | 524 | template <> 525 | inline Hyprlang::STRING* CSimpleConfigValue::ptr() const { 526 | return (Hyprlang::STRING*)p_; 527 | } 528 | 529 | template <> 530 | inline Hyprlang::STRING CSimpleConfigValue::operator*() const { 531 | return *(Hyprlang::STRING*)p_; 532 | } 533 | 534 | template <> 535 | inline Hyprlang::CUSTOMTYPE* CSimpleConfigValue::ptr() const { 536 | return *(Hyprlang::CUSTOMTYPE* const*)p_; 537 | } 538 | 539 | template <> 540 | inline Hyprlang::CUSTOMTYPE CSimpleConfigValue::operator*() const { 541 | std::print("Impossible to implement operator* of CConfigValue, use ptr()"); 542 | abort(); 543 | return *ptr(); 544 | } 545 | }; 546 | 547 | #ifndef HYPRLANG_INTERNAL 548 | #undef HYPRLANG_END_MAGIC 549 | #endif 550 | 551 | #endif 552 | -------------------------------------------------------------------------------- /tests/parse/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | namespace Colors { 8 | constexpr const char* RED = "\x1b[31m"; 9 | constexpr const char* GREEN = "\x1b[32m"; 10 | constexpr const char* YELLOW = "\x1b[33m"; 11 | constexpr const char* BLUE = "\x1b[34m"; 12 | constexpr const char* MAGENTA = "\x1b[35m"; 13 | constexpr const char* CYAN = "\x1b[36m"; 14 | constexpr const char* RESET = "\x1b[0m"; 15 | }; 16 | 17 | #define EXPECT(expr, val) \ 18 | if (const auto RESULT = expr; RESULT != (val)) { \ 19 | std::cout << Colors::RED << "Failed: " << Colors::RESET << #expr << ", expected " << #val << " but got " << RESULT << "\n"; \ 20 | ret = 1; \ 21 | } else { \ 22 | std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got " << val << "\n"; \ 23 | } 24 | 25 | // globals for testing 26 | bool barrelRoll = false; 27 | std::string flagsFound = ""; 28 | Hyprlang::CConfig* pConfig = nullptr; 29 | std::string currentPath = ""; 30 | std::string ignoreKeyword = ""; 31 | std::string useKeyword = ""; 32 | std::string sameKeywordSpecialCat = ""; 33 | bool testHandlerDontOverrideValue = false; 34 | static std::vector categoryKeywordActualValues; 35 | 36 | static Hyprlang::CParseResult handleDoABarrelRoll(const char* COMMAND, const char* VALUE) { 37 | if (std::string(VALUE) == "woohoo, some, params") 38 | barrelRoll = true; 39 | 40 | Hyprlang::CParseResult result; 41 | return result; 42 | } 43 | 44 | static Hyprlang::CParseResult handleFlagsTest(const char* COMMAND, const char* VALUE) { 45 | std::string cmd = COMMAND; 46 | flagsFound = cmd.substr(5); 47 | 48 | Hyprlang::CParseResult result; 49 | return result; 50 | } 51 | 52 | static Hyprlang::CParseResult handleCategoryKeyword(const char* COMMAND, const char* VALUE) { 53 | categoryKeywordActualValues.emplace_back(VALUE); 54 | 55 | return Hyprlang::CParseResult(); 56 | } 57 | 58 | static Hyprlang::CParseResult handleTestIgnoreKeyword(const char* COMMAND, const char* VALUE) { 59 | ignoreKeyword = VALUE; 60 | 61 | return Hyprlang::CParseResult(); 62 | } 63 | 64 | static Hyprlang::CParseResult handleTestUseKeyword(const char* COMMAND, const char* VALUE) { 65 | useKeyword = VALUE; 66 | 67 | return Hyprlang::CParseResult(); 68 | } 69 | 70 | static Hyprlang::CParseResult handleNoop(const char* COMMAND, const char* VALUE) { 71 | return Hyprlang::CParseResult(); 72 | } 73 | 74 | static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE) { 75 | std::string PATH = std::filesystem::canonical(currentPath + "/" + VALUE); 76 | return pConfig->parseFile(PATH.c_str()); 77 | } 78 | 79 | static Hyprlang::CParseResult handleSameKeywordSpecialCat(const char* COMMAND, const char* VALUE) { 80 | sameKeywordSpecialCat = VALUE; 81 | 82 | return Hyprlang::CParseResult(); 83 | } 84 | 85 | static Hyprlang::CParseResult handleTestHandlerDontOverride(const char* COMMAND, const char* VALUE) { 86 | testHandlerDontOverrideValue = true; 87 | 88 | Hyprlang::CParseResult result; 89 | return result; 90 | } 91 | 92 | static Hyprlang::CParseResult handleCustomValueSet(const char* VALUE, void** data) { 93 | if (!*data) 94 | *data = calloc(1, sizeof(int64_t)); 95 | std::string V = VALUE; 96 | if (V == "abc") 97 | *reinterpret_cast(*data) = 1; 98 | else 99 | *reinterpret_cast(*data) = 2; 100 | 101 | Hyprlang::CParseResult result; 102 | return result; 103 | } 104 | 105 | static void handleCustomValueDestroy(void** data) { 106 | if (*data) 107 | free(*data); 108 | } 109 | 110 | int main(int argc, char** argv, char** envp) { 111 | int ret = 0; 112 | 113 | try { 114 | if (!getenv("SHELL")) 115 | setenv("SHELL", "/bin/sh", true); 116 | 117 | setenv("TEST_ENV", "1", true); 118 | 119 | std::cout << "Starting test\n"; 120 | 121 | Hyprlang::CConfig config("./config/config.conf", {}); 122 | pConfig = &config; 123 | currentPath = std::filesystem::canonical("./config/"); 124 | 125 | // setup config 126 | config.addConfigValue("testInt", (Hyprlang::INT)0); 127 | config.addConfigValue("testExpr", (Hyprlang::INT)0); 128 | config.addConfigValue("testEscapedExpr", ""); 129 | config.addConfigValue("testEscapedExpr2", ""); 130 | config.addConfigValue("testEscapedExpr3", ""); 131 | config.addConfigValue("testEscapedEscape", ""); 132 | config.addConfigValue("testMixedEscapedExpression", ""); 133 | config.addConfigValue("testMixedEscapedExpression2", ""); 134 | config.addConfigValue("testImbeddedEscapedExpression", ""); 135 | config.addConfigValue("testDynamicEscapedExpression", ""); 136 | config.addConfigValue("testFloat", 0.F); 137 | config.addConfigValue("testVec", Hyprlang::SVector2D{.x = 69, .y = 420}); 138 | config.addConfigValue("testString", ""); 139 | config.addConfigValue("testStringColon", ""); 140 | config.addConfigValue("testEnv", ""); 141 | config.addConfigValue("testEnv2", ""); 142 | config.addConfigValue("testVar", (Hyprlang::INT)0); 143 | config.addConfigValue("categoryKeyword", (Hyprlang::STRING) ""); 144 | config.addConfigValue("testStringQuotes", ""); 145 | config.addConfigValue("testStringRecursive", ""); 146 | config.addConfigValue("testCategory:testValueInt", (Hyprlang::INT)0); 147 | config.addConfigValue("testCategory:testValueHex", (Hyprlang::INT)0xA); 148 | config.addConfigValue("testCategory:nested1:testValueNest", (Hyprlang::INT)0); 149 | config.addConfigValue("testCategory:nested1:nested2:testValueNest", (Hyprlang::INT)0); 150 | config.addConfigValue("testCategory:nested1:categoryKeyword", (Hyprlang::STRING) ""); 151 | config.addConfigValue("testDefault", (Hyprlang::INT)123); 152 | config.addConfigValue("testCategory:testColor1", (Hyprlang::INT)0); 153 | config.addConfigValue("testCategory:testColor2", (Hyprlang::INT)0); 154 | config.addConfigValue("testCategory:testColor3", (Hyprlang::INT)0); 155 | config.addConfigValue("flagsStuff:value", (Hyprlang::INT)0); 156 | config.addConfigValue("myColors:pink", (Hyprlang::INT)0); 157 | config.addConfigValue("myColors:green", (Hyprlang::INT)0); 158 | config.addConfigValue("myColors:random", (Hyprlang::INT)0); 159 | config.addConfigValue("customType", {Hyprlang::CConfigCustomValueType{&handleCustomValueSet, &handleCustomValueDestroy, "def"}}); 160 | 161 | config.registerHandler(&handleDoABarrelRoll, "doABarrelRoll", {.allowFlags = false}); 162 | config.registerHandler(&handleFlagsTest, "flags", {.allowFlags = true}); 163 | config.registerHandler(&handleSource, "source", {.allowFlags = false}); 164 | config.registerHandler(&handleTestIgnoreKeyword, "testIgnoreKeyword", {.allowFlags = false}); 165 | config.registerHandler(&handleTestUseKeyword, ":testUseKeyword", {.allowFlags = false}); 166 | config.registerHandler(&handleNoop, "testCategory:testUseKeyword", {.allowFlags = false}); 167 | config.registerHandler(&handleCategoryKeyword, "testCategory:categoryKeyword", {.allowFlags = false}); 168 | config.registerHandler(&handleTestHandlerDontOverride, "testHandlerDontOverride", {.allowFlags = false}); 169 | 170 | config.addSpecialCategory("special", {.key = "key"}); 171 | config.addSpecialConfigValue("special", "value", (Hyprlang::INT)0); 172 | 173 | config.addSpecialCategory("specialAnonymous", {.key = nullptr, .ignoreMissing = false, .anonymousKeyBased = true}); 174 | config.addSpecialConfigValue("specialAnonymous", "value", (Hyprlang::INT)0); 175 | config.addSpecialConfigValue("specialAnonymous", "testHandlerDontOverride", (Hyprlang::INT)0); 176 | 177 | config.addSpecialCategory("specialAnonymousNested", {.key = nullptr, .ignoreMissing = false, .anonymousKeyBased = true}); 178 | config.addSpecialConfigValue("specialAnonymousNested", "nested:value1", (Hyprlang::INT)0); 179 | config.addSpecialConfigValue("specialAnonymousNested", "nested:value2", (Hyprlang::INT)0); 180 | config.addSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value1", (Hyprlang::INT)0); 181 | config.addSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value2", (Hyprlang::INT)0); 182 | 183 | config.addConfigValue("multiline", ""); 184 | 185 | config.registerHandler(&handleSameKeywordSpecialCat, "sameKeywordSpecialCat", {.allowFlags = false}); 186 | config.addSpecialCategory("sameKeywordSpecialCat", {.key = nullptr, .ignoreMissing = true, .anonymousKeyBased = false}); 187 | 188 | config.commence(); 189 | 190 | config.addSpecialCategory("sameKeywordSpecialCat:one", {.key = nullptr, .ignoreMissing = true}); 191 | config.addSpecialConfigValue("sameKeywordSpecialCat:one", "some_size", (Hyprlang::INT)10); 192 | config.addSpecialConfigValue("sameKeywordSpecialCat:one", "some_radius", (Hyprlang::FLOAT)0.0); 193 | config.addSpecialCategory("sameKeywordSpecialCat:two", {.key = nullptr, .ignoreMissing = true}); 194 | config.addSpecialConfigValue("sameKeywordSpecialCat:two", "hola", ""); 195 | 196 | config.addSpecialCategory("specialGeneric:one", {.key = nullptr, .ignoreMissing = true}); 197 | config.addSpecialConfigValue("specialGeneric:one", "value", (Hyprlang::INT)0); 198 | config.addSpecialCategory("specialGeneric:two", {.key = nullptr, .ignoreMissing = true}); 199 | config.addSpecialConfigValue("specialGeneric:two", "value", (Hyprlang::INT)0); 200 | 201 | const Hyprlang::CConfigValue copyTest = {(Hyprlang::INT)1}; 202 | config.addSpecialConfigValue("specialGeneric:one", "copyTest", copyTest); 203 | 204 | const auto PARSERESULT = config.parse(); 205 | if (PARSERESULT.error) { 206 | std::cout << "Parse error: " << PARSERESULT.getError() << "\n"; 207 | return 1; 208 | } 209 | 210 | EXPECT(PARSERESULT.error, false); 211 | 212 | // test values 213 | std::cout << " → Testing values\n"; 214 | EXPECT(std::any_cast(config.getConfigValue("testInt")), 123); 215 | EXPECT(std::any_cast(config.getConfigValue("testFloat")), 123.456f); 216 | auto EXP = Hyprlang::SVector2D{.x = 69, .y = 420}; 217 | EXPECT(std::any_cast(config.getConfigValue("testVec")), EXP); 218 | EXPECT(std::any_cast(config.getConfigValue("testString")), std::string{"Hello World! # This is not a comment!"}); 219 | EXPECT(std::any_cast(config.getConfigValue("testStringQuotes")), std::string{"\"Hello World!\""}); 220 | EXPECT(std::any_cast(config.getConfigValue("testCategory:testValueInt")), (Hyprlang::INT)123456); 221 | EXPECT(std::any_cast(config.getConfigValue("testCategory:testValueHex")), (Hyprlang::INT)0xFFFFAABB); 222 | EXPECT(std::any_cast(config.getConfigValue("testCategory:nested1:testValueNest")), (Hyprlang::INT)1); 223 | EXPECT(std::any_cast(config.getConfigValue("testCategory:nested1:nested2:testValueNest")), (Hyprlang::INT)1); 224 | EXPECT(std::any_cast(config.getConfigValue("testDefault")), (Hyprlang::INT)123); 225 | EXPECT(std::any_cast(config.getConfigValue("testCategory:testColor1")), (Hyprlang::INT)0xFFFFFFFF); 226 | EXPECT(std::any_cast(config.getConfigValue("testCategory:testColor2")), (Hyprlang::INT)0xFF000000); 227 | EXPECT(std::any_cast(config.getConfigValue("testCategory:testColor3")), (Hyprlang::INT)0x22ffeeff); 228 | EXPECT(std::any_cast(config.getConfigValue("testStringColon")), std::string{"ee:ee:ee"}); 229 | EXPECT(std::any_cast(config.getConfigValue("categoryKeyword")), std::string{"oops, this one shouldn't call the handler, not fun"}); 230 | EXPECT(std::any_cast(config.getConfigValue("testCategory:nested1:categoryKeyword")), std::string{"this one should not either"}); 231 | EXPECT(ignoreKeyword, "aaa"); 232 | EXPECT(useKeyword, "yes"); 233 | 234 | // test special category with same name as a keyword 235 | EXPECT(sameKeywordSpecialCat, std::string_view{"pablo"}); 236 | EXPECT(std::any_cast(config.getSpecialConfigValue("sameKeywordSpecialCat:one", "some_size")), (Hyprlang::INT)44); 237 | EXPECT(std::any_cast(config.getSpecialConfigValue("sameKeywordSpecialCat:one", "some_radius")), (Hyprlang::FLOAT)7.6); 238 | EXPECT(std::any_cast(config.getSpecialConfigValue("sameKeywordSpecialCat:two", "hola")), std::string_view{"rose"}); 239 | 240 | // Test templated wrapper 241 | auto T1 = Hyprlang::CSimpleConfigValue(&config, "testInt"); 242 | auto T2 = Hyprlang::CSimpleConfigValue(&config, "testFloat"); 243 | auto T3 = Hyprlang::CSimpleConfigValue(&config, "testVec"); 244 | auto T4 = Hyprlang::CSimpleConfigValue(&config, "testString"); 245 | EXPECT(*T1, 123); 246 | EXPECT(*T2, 123.456F); 247 | EXPECT(*T3, EXP); 248 | EXPECT(*T4, "Hello World! # This is not a comment!"); 249 | 250 | // test expressions 251 | std::cout << " → Testing expressions\n"; 252 | EXPECT(std::any_cast(config.getConfigValue("testExpr")), 1335); 253 | 254 | // test expression escape 255 | std::cout << " → Testing expression escapes\n"; 256 | EXPECT(std::any_cast(config.getConfigValue("testEscapedExpr")), std::string{"{{testInt + 7}}"}); 257 | EXPECT(std::any_cast(config.getConfigValue("testEscapedExpr2")), std::string{"{{testInt + 7}}"}); 258 | EXPECT(std::any_cast(config.getConfigValue("testEscapedExpr3")), std::string{"{{3 + 8}}"}); 259 | EXPECT(std::any_cast(config.getConfigValue("testEscapedEscape")), std::string{"\\5"}); 260 | EXPECT(std::any_cast(config.getConfigValue("testMixedEscapedExpression")), std::string{"-2 {{ {{50 + 50}} / {{10 * 5}} }}"}); 261 | EXPECT(std::any_cast(config.getConfigValue("testMixedEscapedExpression2")), std::string{"{{8\\13}} should equal \"{{8\\13}}\""}); 262 | 263 | EXPECT(std::any_cast(config.getConfigValue("testImbeddedEscapedExpression")), std::string{"{{10 + 10}}"}); 264 | EXPECT(std::any_cast(config.getConfigValue("testDynamicEscapedExpression")), std::string{"{{ moved: 500 expr: {{1000 / 2}} }}"}); 265 | 266 | // test static values 267 | std::cout << " → Testing static values\n"; 268 | static auto* const PTESTINT = config.getConfigValuePtr("testInt")->getDataStaticPtr(); 269 | EXPECT(*reinterpret_cast(*PTESTINT), 123); 270 | config.parse(); 271 | EXPECT(*reinterpret_cast(*PTESTINT), 123); 272 | 273 | // test handlers 274 | std::cout << " → Testing handlers\n"; 275 | EXPECT(barrelRoll, true); 276 | EXPECT(flagsFound, std::string{"abc"}); 277 | EXPECT(testHandlerDontOverrideValue, false); 278 | 279 | EXPECT(categoryKeywordActualValues.at(0), "we are having fun"); 280 | EXPECT(categoryKeywordActualValues.at(1), "so much fun"); 281 | EXPECT(categoryKeywordActualValues.at(2), "im the fun one at parties"); 282 | 283 | // test dynamic 284 | std::cout << " → Testing dynamic\n"; 285 | barrelRoll = false; 286 | EXPECT(config.parseDynamic("doABarrelRoll = woohoo, some, params").error, false); 287 | EXPECT(barrelRoll, true); 288 | EXPECT(config.parseDynamic("testCategory:testValueHex", "0xaabbccdd").error, false); 289 | EXPECT(std::any_cast(config.getConfigValue("testCategory:testValueHex")), (Hyprlang::INT)0xAABBCCDD); 290 | EXPECT(config.parseDynamic("testStringColon", "1:3:3:7").error, false); 291 | EXPECT(std::any_cast(config.getConfigValue("testStringColon")), std::string{"1:3:3:7"}); 292 | EXPECT(config.parseDynamic("flagsStuff:value = 69").error, false); 293 | EXPECT(std::any_cast(config.getConfigValue("flagsStuff:value")), (Hyprlang::INT)69); 294 | 295 | // test dynamic special 296 | config.addSpecialConfigValue("specialGeneric:one", "boom", (Hyprlang::INT)0); 297 | EXPECT(config.parseDynamic("specialGeneric:one:boom = 1").error, false); 298 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialGeneric:one", "boom")), (Hyprlang::INT)1); 299 | 300 | // test variables 301 | std::cout << " → Testing variables\n"; 302 | EXPECT(std::any_cast(config.getConfigValue("testVar")), 13371337); 303 | EXPECT(std::any_cast(config.getConfigValue("testStringRecursive")), std::string{"abc"}); 304 | 305 | // test dynamic variables 306 | std::cout << " → Testing dynamic variables\n"; 307 | EXPECT(config.parseDynamic("$MY_VAR_2 = 420").error, false); 308 | EXPECT(std::any_cast(config.getConfigValue("testVar")), 1337420); 309 | 310 | EXPECT(config.parseDynamic("$RECURSIVE1 = d").error, false); 311 | EXPECT(std::any_cast(config.getConfigValue("testStringRecursive")), std::string{"dbc"}); 312 | 313 | // test expression escape with dynamic vars 314 | EXPECT(config.parseDynamic("$MOVING_VAR = 500").error, false); 315 | EXPECT(std::any_cast(config.getConfigValue("testDynamicEscapedExpression")), std::string{"{{ moved: 250 expr: {{500 / 2}} }}"}); 316 | 317 | // test dynamic exprs 318 | EXPECT(config.parseDynamic("testExpr = {{EXPR_VAR * 2}}").error, false); 319 | EXPECT(std::any_cast(config.getConfigValue("testExpr")), 1339L * 2); 320 | 321 | // test env variables 322 | std::cout << " → Testing env variables\n"; 323 | EXPECT(std::any_cast(config.getConfigValue("testEnv")), std::string{getenv("SHELL")}); 324 | EXPECT(std::any_cast(config.getConfigValue("testEnv2")), std::string{"1"}); 325 | 326 | // test special categories 327 | std::cout << " → Testing special categories\n"; 328 | EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "a")), 1); 329 | EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "b")), 2); 330 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialGeneric:one", "value")), 1); 331 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialGeneric:two", "value")), 2); 332 | EXPECT(config.parseDynamic("special[b]:value = 3").error, false); 333 | EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "b")), 3); 334 | EXPECT(config.parseDynamic("specialAnonymousNested[c]:nested:value1 = 4").error, false); 335 | EXPECT(config.parseDynamic("specialAnonymousNested[c]:nested:value2 = 5").error, false); 336 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value1", "c")), (Hyprlang::INT)4); 337 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", "c")), (Hyprlang::INT)5); 338 | EXPECT(config.parseDynamic("specialAnonymousNested[c]:nested:value2 = 6").error, false); 339 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", "c")), (Hyprlang::INT)6); 340 | 341 | EXPECT(config.parseDynamic("special[a]:value = 69").error, false); 342 | EXPECT(config.parseDynamic("special[b]:value = 420").error, false); 343 | EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "a")), 69); 344 | EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "b")), 420); 345 | 346 | // test dynamic special variable 347 | EXPECT(config.parseDynamic("$SPECIALVAL1 = 2").error, false); 348 | EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "a")), (Hyprlang::INT)2); 349 | 350 | // test copying 351 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialGeneric:one", "copyTest")), 2); 352 | 353 | // test listing keys 354 | EXPECT(config.listKeysForSpecialCategory("special")[1], "b"); 355 | 356 | // test anonymous 357 | EXPECT(config.listKeysForSpecialCategory("specialAnonymous").size(), 2); 358 | const auto KEYS = config.listKeysForSpecialCategory("specialAnonymous"); 359 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymous", "value", KEYS[0].c_str())), 2); 360 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymous", "testHandlerDontOverride", KEYS[0].c_str())), 1); 361 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymous", "value", KEYS[1].c_str())), 3); 362 | 363 | // test anonymous nested 364 | EXPECT(config.listKeysForSpecialCategory("specialAnonymousNested").size(), 2 + /*from dynamic*/ 1); 365 | const auto KEYS2 = config.listKeysForSpecialCategory("specialAnonymousNested"); 366 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value1", KEYS2[0].c_str())), 1); 367 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", KEYS2[0].c_str())), 2); 368 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value1", KEYS2[1].c_str())), 3); 369 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested:value2", KEYS2[1].c_str())), 4); 370 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value1", KEYS2[0].c_str())), 10); 371 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value2", KEYS2[0].c_str())), 11); 372 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value1", KEYS2[1].c_str())), 12); 373 | EXPECT(std::any_cast(config.getSpecialConfigValue("specialAnonymousNested", "nested1:nested2:value2", KEYS2[1].c_str())), 13); 374 | 375 | // test sourcing 376 | std::cout << " → Testing sourcing\n"; 377 | EXPECT(std::any_cast(config.getConfigValue("myColors:pink")), (Hyprlang::INT)0xFFc800c8); 378 | EXPECT(std::any_cast(config.getConfigValue("myColors:green")), (Hyprlang::INT)0xFF14f014); 379 | EXPECT(std::any_cast(config.getConfigValue("myColors:random")), (Hyprlang::INT)0xFFFF1337); 380 | 381 | // test custom type 382 | std::cout << " → Testing custom types\n"; 383 | EXPECT(*reinterpret_cast(std::any_cast(config.getConfigValue("customType"))), (Hyprlang::INT)1); 384 | 385 | // test multiline config 386 | EXPECT(std::any_cast(config.getConfigValue("multiline")), std::string{"very long command"}); 387 | 388 | // test dynamic env 389 | setenv("TEST_ENV", "2", true); 390 | config.parse(); 391 | std::cout << " → Testing dynamic env variables\n"; 392 | EXPECT(std::any_cast(config.getConfigValue("testEnv2")), std::string{"2"}); 393 | 394 | std::cout << " → Testing error.conf\n"; 395 | Hyprlang::CConfig errorConfig("./config/error.conf", {.verifyOnly = true, .throwAllErrors = true}); 396 | 397 | errorConfig.commence(); 398 | const auto ERRORS = errorConfig.parse(); 399 | 400 | EXPECT(ERRORS.error, true); 401 | const auto ERRORSTR = std::string{ERRORS.getError()}; 402 | EXPECT(std::count(ERRORSTR.begin(), ERRORSTR.end(), '\n'), 1); 403 | 404 | std::cout << " → Testing invalid-numbers.conf\n"; 405 | Hyprlang::CConfig invalidNumbersConfig("./config/invalid-numbers.conf", {.throwAllErrors = true}); 406 | invalidNumbersConfig.addConfigValue("invalidHex", (Hyprlang::INT)0); 407 | invalidNumbersConfig.addConfigValue("emptyHex", (Hyprlang::INT)0); 408 | invalidNumbersConfig.addConfigValue("hugeHex", (Hyprlang::INT)0); 409 | invalidNumbersConfig.addConfigValue("invalidInt", (Hyprlang::INT)0); 410 | invalidNumbersConfig.addConfigValue("emptyInt", (Hyprlang::INT)0); 411 | invalidNumbersConfig.addConfigValue("invalidColor", (Hyprlang::INT)0); 412 | invalidNumbersConfig.addConfigValue("invalidFirstCharColor", (Hyprlang::INT)0); 413 | invalidNumbersConfig.addConfigValue("invalidColorAlpha", (Hyprlang::INT)0); 414 | invalidNumbersConfig.addConfigValue("invalidFirstCharColorAlpha", (Hyprlang::INT)0); 415 | 416 | invalidNumbersConfig.commence(); 417 | const auto ERRORS2 = invalidNumbersConfig.parse(); 418 | 419 | EXPECT(ERRORS2.error, true); 420 | const auto ERRORSTR2 = std::string{ERRORS2.getError()}; 421 | EXPECT(std::count(ERRORSTR2.begin(), ERRORSTR2.end(), '\n'), 9 - 1); 422 | 423 | Hyprlang::CConfig multilineErrorConfig("./config/multiline-errors.conf", {.verifyOnly = true, .throwAllErrors = true}); 424 | multilineErrorConfig.commence(); 425 | const auto ERRORS3 = multilineErrorConfig.parse(); 426 | EXPECT(ERRORS3.error, true); 427 | const auto ERRORSTR3 = std::string{ERRORS3.getError()}; 428 | 429 | // Error on line 12 430 | EXPECT(ERRORSTR3.contains("12"), true); 431 | // Backslash at end of file 432 | EXPECT(ERRORSTR3.contains("backslash"), true); 433 | } catch (const char* e) { 434 | std::cout << Colors::RED << "Error: " << Colors::RESET << e << "\n"; 435 | return 1; 436 | } 437 | 438 | return ret; 439 | } 440 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | using namespace Hyprlang; 20 | using namespace Hyprutils::String; 21 | 22 | #ifdef __APPLE__ 23 | #include 24 | #define environ (*_NSGetEnviron()) 25 | #else 26 | // NOLINTNEXTLINE 27 | extern "C" char** environ; 28 | #endif 29 | 30 | // defines 31 | inline constexpr const char* ANONYMOUS_KEY = "__hyprlang_internal_anonymous_key"; 32 | inline constexpr const char* MULTILINE_SPACE_CHARSET = " \t"; 33 | // 34 | 35 | static size_t seekABIStructSize(const void* begin, size_t startOffset, size_t maxSize) { 36 | for (size_t off = startOffset; off < maxSize; off += 4) { 37 | if (*(int*)((unsigned char*)begin + off) == HYPRLANG_END_MAGIC) 38 | return off; 39 | } 40 | 41 | return 0; 42 | } 43 | 44 | static std::expected getNextLine(std::istream& str, int& rawLineNum, int& lineNum) { 45 | std::string line = ""; 46 | std::string nextLine = ""; 47 | 48 | if (!std::getline(str, line)) 49 | return std::unexpected(GETNEXTLINEFAILURE_EOF); 50 | 51 | lineNum = ++rawLineNum; 52 | 53 | while (line.length() > 0 && line.at(line.length() - 1) == '\\') { 54 | const auto lastNonSpace = line.length() < 2 ? -1 : line.find_last_not_of(MULTILINE_SPACE_CHARSET, line.length() - 2); 55 | line = line.substr(0, lastNonSpace + 1); 56 | 57 | if (!std::getline(str, nextLine)) 58 | return std::unexpected(GETNEXTLINEFAILURE_BACKSLASH); 59 | 60 | ++rawLineNum; 61 | line += nextLine; 62 | } 63 | 64 | return line; 65 | } 66 | 67 | CConfig::CConfig(const char* path, const Hyprlang::SConfigOptions& options_) : impl(new CConfigImpl) { 68 | SConfigOptions options; 69 | std::memcpy(&options, &options_, seekABIStructSize(&options_, 16, sizeof(SConfigOptions))); 70 | 71 | if (options.pathIsStream) 72 | impl->rawConfigString = path; 73 | else 74 | impl->path = path; 75 | 76 | if (!options.pathIsStream && !std::filesystem::exists(impl->path)) { 77 | if (!options.allowMissingConfig) 78 | throw "File does not exist"; 79 | } 80 | 81 | impl->recheckEnv(); 82 | 83 | std::ranges::sort(impl->envVariables, [&](const auto& a, const auto& b) { return a.name.length() > b.name.length(); }); 84 | 85 | impl->configOptions = options; 86 | } 87 | 88 | CConfig::~CConfig() { 89 | delete impl; 90 | } 91 | 92 | void CConfig::addConfigValue(const char* name, const CConfigValue& value) { 93 | if (m_bCommenced) 94 | throw "Cannot addConfigValue after commence()"; 95 | 96 | if ((eDataType)value.m_eType != CONFIGDATATYPE_CUSTOM && (eDataType)value.m_eType != CONFIGDATATYPE_STR) 97 | impl->defaultValues.emplace(name, SConfigDefaultValue{.data = value.getValue(), .type = (eDataType)value.m_eType}); 98 | else if ((eDataType)value.m_eType == CONFIGDATATYPE_STR) 99 | impl->defaultValues.emplace(name, SConfigDefaultValue{.data = std::string{std::any_cast(value.getValue())}, .type = (eDataType)value.m_eType}); 100 | else 101 | impl->defaultValues.emplace(name, 102 | SConfigDefaultValue{.data = reinterpret_cast(value.m_pData)->defaultVal, 103 | .type = (eDataType)value.m_eType, 104 | .handler = reinterpret_cast(value.m_pData)->handler, 105 | .dtor = reinterpret_cast(value.m_pData)->dtor}); 106 | } 107 | 108 | void CConfig::addSpecialConfigValue(const char* cat, const char* name, const CConfigValue& value) { 109 | const auto IT = std::ranges::find_if(impl->specialCategoryDescriptors, [&](const auto& other) { return other->name == cat; }); 110 | 111 | if (IT == impl->specialCategoryDescriptors.end()) 112 | throw "No such category"; 113 | 114 | if ((eDataType)value.m_eType != CONFIGDATATYPE_CUSTOM && (eDataType)value.m_eType != CONFIGDATATYPE_STR) 115 | IT->get()->defaultValues.emplace(name, SConfigDefaultValue{.data = value.getValue(), .type = (eDataType)value.m_eType}); 116 | else if ((eDataType)value.m_eType == CONFIGDATATYPE_STR) 117 | IT->get()->defaultValues.emplace(name, SConfigDefaultValue{.data = std::string{std::any_cast(value.getValue())}, .type = (eDataType)value.m_eType}); 118 | else 119 | IT->get()->defaultValues.emplace(name, 120 | SConfigDefaultValue{.data = reinterpret_cast(value.m_pData)->defaultVal, 121 | .type = (eDataType)value.m_eType, 122 | .handler = reinterpret_cast(value.m_pData)->handler, 123 | .dtor = reinterpret_cast(value.m_pData)->dtor}); 124 | 125 | const auto CAT = std::ranges::find_if(impl->specialCategories, [cat](const auto& other) { return other->name == cat && other->isStatic; }); 126 | 127 | if (CAT != impl->specialCategories.end()) 128 | CAT->get()->values[name].defaultFrom(IT->get()->defaultValues[name]); 129 | } 130 | 131 | void CConfig::removeSpecialConfigValue(const char* cat, const char* name) { 132 | const auto IT = std::ranges::find_if(impl->specialCategoryDescriptors, [&](const auto& other) { return other->name == cat; }); 133 | 134 | if (IT == impl->specialCategoryDescriptors.end()) 135 | throw "No such category"; 136 | 137 | std::erase_if(IT->get()->defaultValues, [name](const auto& other) { return other.first == name; }); 138 | } 139 | 140 | void CConfig::addSpecialCategory(const char* name, SSpecialCategoryOptions options_) { 141 | SSpecialCategoryOptions options; 142 | std::memcpy(&options, &options_, seekABIStructSize(&options_, 8, sizeof(SSpecialCategoryOptions))); 143 | 144 | const auto PDESC = impl->specialCategoryDescriptors.emplace_back(std::make_unique()).get(); 145 | PDESC->name = name; 146 | PDESC->key = options.key ? options.key : ""; 147 | PDESC->dontErrorOnMissing = options.ignoreMissing; 148 | 149 | if (!options.key && !options.anonymousKeyBased) { 150 | const auto PCAT = impl->specialCategories.emplace_back(std::make_unique()).get(); 151 | PCAT->descriptor = PDESC; 152 | PCAT->name = name; 153 | PCAT->key = ""; 154 | PCAT->isStatic = true; 155 | } 156 | 157 | if (options.anonymousKeyBased) { 158 | PDESC->key = ANONYMOUS_KEY; 159 | PDESC->anonymous = true; 160 | } 161 | 162 | // sort longest to shortest 163 | std::ranges::sort(impl->specialCategories, [](const auto& a, const auto& b) -> int { return a->name.length() > b->name.length(); }); 164 | std::ranges::sort(impl->specialCategoryDescriptors, [](const auto& a, const auto& b) -> int { return a->name.length() > b->name.length(); }); 165 | } 166 | 167 | void CConfig::removeSpecialCategory(const char* name) { 168 | std::erase_if(impl->specialCategories, [name](const auto& other) { return other->name == name; }); 169 | std::erase_if(impl->specialCategoryDescriptors, [name](const auto& other) { return other->name == name; }); 170 | } 171 | 172 | void CConfig::applyDefaultsToCat(SSpecialCategory& cat) { 173 | for (auto& [k, v] : cat.descriptor->defaultValues) { 174 | cat.values[k].defaultFrom(v); 175 | } 176 | } 177 | 178 | void CConfig::commence() { 179 | m_bCommenced = true; 180 | for (auto& [k, v] : impl->defaultValues) { 181 | impl->values[k].defaultFrom(v); 182 | } 183 | } 184 | 185 | static std::expected configStringToInt(const std::string& VALUE) { 186 | auto parseHex = [](const std::string& value) -> std::expected { 187 | try { 188 | size_t position; 189 | auto result = stoll(value, &position, 16); 190 | if (position == value.size()) 191 | return result; 192 | } catch (const std::exception&) {} 193 | return std::unexpected("invalid hex " + value); 194 | }; 195 | if (VALUE.starts_with("0x")) { 196 | // Values with 0x are hex 197 | return parseHex(VALUE); 198 | } else if (VALUE.starts_with("rgba(") && VALUE.ends_with(')')) { 199 | const auto VALUEWITHOUTFUNC = trim(VALUE.substr(5, VALUE.length() - 6)); 200 | 201 | // try doing it the comma way first 202 | if (std::count(VALUEWITHOUTFUNC.begin(), VALUEWITHOUTFUNC.end(), ',') == 3) { 203 | // cool 204 | std::string rolling = VALUEWITHOUTFUNC; 205 | auto r = configStringToInt(trim(rolling.substr(0, rolling.find(',')))); 206 | rolling = rolling.substr(rolling.find(',') + 1); 207 | auto g = configStringToInt(trim(rolling.substr(0, rolling.find(',')))); 208 | rolling = rolling.substr(rolling.find(',') + 1); 209 | auto b = configStringToInt(trim(rolling.substr(0, rolling.find(',')))); 210 | rolling = rolling.substr(rolling.find(',') + 1); 211 | uint8_t a = 0; 212 | try { 213 | a = std::round(std::stof(trim(rolling.substr(0, rolling.find(',')))) * 255.f); 214 | } catch (std::exception& e) { return std::unexpected("failed parsing " + VALUEWITHOUTFUNC); } 215 | 216 | if (!r.has_value() || !g.has_value() || !b.has_value()) 217 | return std::unexpected("failed parsing " + VALUEWITHOUTFUNC); 218 | 219 | return (a * (Hyprlang::INT)0x1000000) + (r.value() * (Hyprlang::INT)0x10000) + (g.value() * (Hyprlang::INT)0x100) + b.value(); 220 | } else if (VALUEWITHOUTFUNC.length() == 8) { 221 | const auto RGBA = parseHex(VALUEWITHOUTFUNC); 222 | 223 | if (!RGBA.has_value()) 224 | return RGBA; 225 | 226 | // now we need to RGBA -> ARGB. The config holds ARGB only. 227 | return (RGBA.value() >> 8) + (0x1000000 * (RGBA.value() & 0xFF)); 228 | } 229 | 230 | return std::unexpected("rgba() expects length of 8 characters (4 bytes) or 4 comma separated values"); 231 | 232 | } else if (VALUE.starts_with("rgb(") && VALUE.ends_with(')')) { 233 | const auto VALUEWITHOUTFUNC = trim(VALUE.substr(4, VALUE.length() - 5)); 234 | 235 | // try doing it the comma way first 236 | if (std::count(VALUEWITHOUTFUNC.begin(), VALUEWITHOUTFUNC.end(), ',') == 2) { 237 | // cool 238 | std::string rolling = VALUEWITHOUTFUNC; 239 | auto r = configStringToInt(trim(rolling.substr(0, rolling.find(',')))); 240 | rolling = rolling.substr(rolling.find(',') + 1); 241 | auto g = configStringToInt(trim(rolling.substr(0, rolling.find(',')))); 242 | rolling = rolling.substr(rolling.find(',') + 1); 243 | auto b = configStringToInt(trim(rolling.substr(0, rolling.find(',')))); 244 | 245 | if (!r.has_value() || !g.has_value() || !b.has_value()) 246 | return std::unexpected("failed parsing " + VALUEWITHOUTFUNC); 247 | 248 | return (Hyprlang::INT)0xFF000000 + (r.value() * (Hyprlang::INT)0x10000) + (g.value() * (Hyprlang::INT)0x100) + b.value(); 249 | } else if (VALUEWITHOUTFUNC.length() == 6) { 250 | const auto RGB = parseHex(VALUEWITHOUTFUNC); 251 | 252 | if (!RGB.has_value()) 253 | return RGB; 254 | 255 | return RGB.value() + 0xFF000000; 256 | } 257 | 258 | return std::unexpected("rgb() expects length of 6 characters (3 bytes) or 3 comma separated values"); 259 | } else if (VALUE.starts_with("true") || VALUE.starts_with("on") || VALUE.starts_with("yes")) { 260 | return 1; 261 | } else if (VALUE.starts_with("false") || VALUE.starts_with("off") || VALUE.starts_with("no")) { 262 | return 0; 263 | } 264 | 265 | if (VALUE.empty() || !isNumber(VALUE, false)) 266 | return std::unexpected("cannot parse \"" + VALUE + "\" as an int."); 267 | 268 | try { 269 | const auto RES = std::stoll(VALUE); 270 | return RES; 271 | } catch (std::exception& e) { return std::unexpected(std::string{"stoll threw: "} + e.what()); } 272 | 273 | return 0; 274 | } 275 | 276 | // found, result 277 | std::pair CConfig::configSetValueSafe(const std::string& command, const std::string& value) { 278 | CParseResult result; 279 | 280 | std::string valueName; 281 | for (auto& c : impl->categories) { 282 | valueName += c + ':'; 283 | } 284 | 285 | valueName += command; 286 | 287 | // TODO: all this sucks xD 288 | 289 | SSpecialCategory* overrideSpecialCat = nullptr; 290 | 291 | if (valueName.contains('[') && valueName.contains(']')) { 292 | const auto L = valueName.find_first_of('['); 293 | const auto R = valueName.find_last_of(']'); 294 | 295 | if (L < R) { 296 | const auto CATKEY = valueName.substr(L + 1, R - L - 1); 297 | impl->currentSpecialKey = CATKEY; 298 | 299 | valueName = valueName.substr(0, L) + valueName.substr(R + 1); 300 | 301 | for (auto& sc : impl->specialCategoryDescriptors) { 302 | if (sc->key.empty() || !valueName.starts_with(sc->name + ":")) 303 | continue; 304 | 305 | bool keyExists = false; 306 | for (const auto& specialCat : impl->specialCategories) { 307 | if (specialCat->key != sc->key || specialCat->name != sc->name) 308 | continue; 309 | 310 | if (CATKEY != std::string_view{std::any_cast(specialCat->values[sc->key].getValue())}) 311 | continue; 312 | 313 | // existing special 314 | keyExists = true; 315 | overrideSpecialCat = specialCat.get(); 316 | } 317 | 318 | if (keyExists) 319 | break; 320 | 321 | // if it doesn't exist, make it 322 | const auto PCAT = impl->specialCategories.emplace_back(std::make_unique()).get(); 323 | PCAT->descriptor = sc.get(); 324 | PCAT->name = sc->name; 325 | PCAT->key = sc->key; 326 | addSpecialConfigValue(sc->name.c_str(), sc->key.c_str(), CConfigValue(CATKEY.c_str())); 327 | 328 | applyDefaultsToCat(*PCAT); 329 | 330 | PCAT->values[sc->key].setFrom(CATKEY); 331 | overrideSpecialCat = PCAT; 332 | break; 333 | } 334 | } 335 | } 336 | 337 | auto VALUEIT = impl->values.find(valueName); 338 | if (VALUEIT == impl->values.end()) { 339 | // it might be in a special category 340 | bool found = false; 341 | 342 | if (overrideSpecialCat) { 343 | VALUEIT = overrideSpecialCat->values.find(valueName.substr(overrideSpecialCat->name.length() + 1)); 344 | 345 | if (VALUEIT != overrideSpecialCat->values.end()) 346 | found = true; 347 | } else { 348 | if (impl->currentSpecialCategory && valueName.starts_with(impl->currentSpecialCategory->name)) { 349 | VALUEIT = impl->currentSpecialCategory->values.find(valueName.substr(impl->currentSpecialCategory->name.length() + 1)); 350 | 351 | if (VALUEIT != impl->currentSpecialCategory->values.end()) 352 | found = true; 353 | } 354 | 355 | // probably a handler 356 | if (!valueName.contains(":")) 357 | return {false, result}; 358 | 359 | if (!found) { 360 | for (auto& sc : impl->specialCategories) { 361 | if (!valueName.starts_with(sc->name + ":")) 362 | continue; 363 | 364 | if (!sc->isStatic && std::string{std::any_cast(sc->values[sc->key].getValue())} != impl->currentSpecialKey) 365 | continue; 366 | 367 | VALUEIT = sc->values.find(valueName.substr(sc->name.length() + 1)); 368 | impl->currentSpecialCategory = sc.get(); 369 | 370 | if (VALUEIT != sc->values.end()) 371 | found = true; 372 | else if (sc->descriptor->dontErrorOnMissing) 373 | return {false, result}; // will return a success, cuz we want to ignore missing 374 | 375 | break; 376 | } 377 | } 378 | 379 | if (!found) { 380 | // could be a dynamic category that doesnt exist yet 381 | for (auto& sc : impl->specialCategoryDescriptors) { 382 | if (sc->key.empty() || !valueName.starts_with(sc->name + ":")) 383 | continue; 384 | 385 | // found value root to be a special category, get the trunk 386 | const auto VALUETRUNK = valueName.substr(sc->name.length() + 1); 387 | 388 | // check if trunk is a value within the special category 389 | if (!sc->defaultValues.contains(VALUETRUNK) && VALUETRUNK != sc->key) 390 | break; 391 | 392 | // bingo 393 | const auto PCAT = impl->specialCategories.emplace_back(std::make_unique()).get(); 394 | PCAT->descriptor = sc.get(); 395 | PCAT->name = sc->name; 396 | PCAT->key = sc->key; 397 | addSpecialConfigValue(sc->name.c_str(), sc->key.c_str(), CConfigValue("0")); 398 | 399 | applyDefaultsToCat(*PCAT); 400 | 401 | VALUEIT = PCAT->values.find(valueName.substr(sc->name.length() + 1)); 402 | impl->currentSpecialCategory = PCAT; 403 | 404 | if (VALUEIT != PCAT->values.end()) 405 | found = true; 406 | 407 | if (sc->anonymous) { 408 | // find suitable key 409 | size_t biggest = 0; 410 | for (auto& catt : impl->specialCategories) { 411 | biggest = std::max(catt->anonymousID, biggest); 412 | } 413 | 414 | biggest++; 415 | 416 | PCAT->values[ANONYMOUS_KEY].setFrom(std::to_string(biggest)); 417 | impl->currentSpecialKey = std::to_string(biggest); 418 | PCAT->anonymousID = biggest; 419 | } else { 420 | if (VALUEIT == PCAT->values.end() || VALUEIT->first != sc->key) { 421 | result.setError(std::format("special category's first value must be the key. Key for <{}> is <{}>", PCAT->name, PCAT->key)); 422 | return {true, result}; 423 | } 424 | impl->currentSpecialKey = value; 425 | } 426 | 427 | break; 428 | } 429 | } 430 | } 431 | 432 | if (!found) { 433 | result.setError(std::format("config option <{}> does not exist.", valueName)); 434 | return {false, result}; 435 | } 436 | } 437 | 438 | switch (VALUEIT->second.m_eType) { 439 | case CConfigValue::eDataType::CONFIGDATATYPE_INT: { 440 | 441 | const auto INT = configStringToInt(value); 442 | if (!INT.has_value()) { 443 | result.setError(INT.error()); 444 | return {true, result}; 445 | } 446 | 447 | VALUEIT->second.setFrom(INT.value()); 448 | 449 | break; 450 | } 451 | case CConfigValue::eDataType::CONFIGDATATYPE_FLOAT: { 452 | try { 453 | VALUEIT->second.setFrom(std::stof(value)); 454 | } catch (std::exception& e) { 455 | result.setError(std::format("failed parsing a float: {}", e.what())); 456 | return {true, result}; 457 | } 458 | break; 459 | } 460 | case CConfigValue::eDataType::CONFIGDATATYPE_VEC2: { 461 | try { 462 | const auto SPACEPOS = value.find(' '); 463 | if (SPACEPOS == std::string::npos) 464 | throw std::runtime_error("no space"); 465 | const auto LHS = value.substr(0, SPACEPOS); 466 | const auto RHS = value.substr(SPACEPOS + 1); 467 | 468 | if (LHS.contains(" ") || RHS.contains(" ")) 469 | throw std::runtime_error("too many args"); 470 | 471 | VALUEIT->second.setFrom(SVector2D{.x = std::stof(LHS), .y = std::stof(RHS)}); 472 | } catch (std::exception& e) { 473 | result.setError(std::format("failed parsing a vec2: {}", e.what())); 474 | return {true, result}; 475 | } 476 | break; 477 | } 478 | case CConfigValue::eDataType::CONFIGDATATYPE_STR: { 479 | VALUEIT->second.setFrom(value); 480 | break; 481 | } 482 | case CConfigValue::eDataType::CONFIGDATATYPE_CUSTOM: { 483 | auto RESULT = reinterpret_cast(VALUEIT->second.m_pData) 484 | ->handler(value.c_str(), &reinterpret_cast(VALUEIT->second.m_pData)->data); 485 | reinterpret_cast(VALUEIT->second.m_pData)->lastVal = value; 486 | 487 | if (RESULT.error) { 488 | result.setError(RESULT.getError()); 489 | return {true, result}; 490 | } 491 | break; 492 | } 493 | default: { 494 | result.setError("internal error: invalid value found (no type?)"); 495 | return {true, result}; 496 | } 497 | } 498 | 499 | VALUEIT->second.m_bSetByUser = true; 500 | 501 | return {true, result}; 502 | } 503 | 504 | CParseResult CConfig::parseVariable(const std::string& lhs, const std::string& rhs, bool dynamic) { 505 | auto IT = std::ranges::find_if(impl->variables, [&](const auto& v) { return v.name == lhs.substr(1); }); 506 | 507 | if (IT != impl->variables.end()) 508 | IT->value = rhs; 509 | else { 510 | impl->variables.push_back({lhs.substr(1), rhs}); 511 | std::ranges::sort(impl->variables, [](const auto& lhs, const auto& rhs) { return lhs.name.length() > rhs.name.length(); }); 512 | IT = std::ranges::find_if(impl->variables, [&](const auto& v) { return v.name == lhs.substr(1); }); 513 | } 514 | 515 | if (dynamic) { 516 | for (auto& l : IT->linesContainingVar) { 517 | impl->categories = l.categories; 518 | impl->currentSpecialCategory = l.specialCategory; 519 | parseLine(l.line, true); 520 | } 521 | 522 | impl->categories = {}; 523 | } 524 | 525 | CParseResult result; 526 | return result; 527 | } 528 | 529 | void CConfigImpl::recheckEnv() { 530 | envVariables.clear(); 531 | for (char** env = environ; *env; ++env) { 532 | const std::string ENVVAR = *env ? *env : ""; 533 | const auto VARIABLE = ENVVAR.substr(0, ENVVAR.find_first_of('=')); 534 | const auto VALUE = ENVVAR.substr(ENVVAR.find_first_of('=') + 1); 535 | envVariables.push_back({VARIABLE, VALUE}); 536 | } 537 | } 538 | 539 | SVariable* CConfigImpl::getVariable(const std::string& name) { 540 | for (auto& v : envVariables) { 541 | if (v.name == name) 542 | return &v; 543 | } 544 | 545 | for (auto& v : variables) { 546 | if (v.name == name) 547 | return &v; 548 | } 549 | 550 | return nullptr; 551 | } 552 | 553 | std::optional CConfigImpl::parseComment(const std::string& comment) { 554 | const auto COMMENT = trim(comment); 555 | 556 | if (!COMMENT.starts_with("hyprlang")) 557 | return std::nullopt; 558 | 559 | CConstVarList args(COMMENT, 0, 's', true); 560 | 561 | bool negated = false; 562 | std::string ifBlockVariable = ""; 563 | 564 | for (size_t i = 1; i < args.size(); ++i) { 565 | if (args[i] == "noerror") { 566 | if (negated) 567 | currentFlags.noError = false; 568 | else 569 | currentFlags.noError = args[2] == "true" || args[2] == "yes" || args[2] == "enable" || args[2] == "enabled" || args[2] == "set" || args[2].empty(); 570 | break; 571 | } 572 | 573 | if (args[i] == "endif") { 574 | if (currentFlags.ifDatas.empty()) 575 | return "stray endif"; 576 | currentFlags.ifDatas.pop_back(); 577 | break; 578 | } 579 | 580 | if (args[i] == "if") { 581 | ifBlockVariable = args[++i]; 582 | break; 583 | } 584 | } 585 | 586 | if (!ifBlockVariable.empty()) { 587 | if (ifBlockVariable.starts_with("!")) { 588 | negated = true; 589 | ifBlockVariable = ifBlockVariable.substr(1); 590 | } 591 | 592 | CConfigImpl::SIfBlockData newIfData; 593 | 594 | if (const auto VAR = getVariable(ifBlockVariable); VAR) 595 | newIfData.failed = negated ? VAR->truthy() : !VAR->truthy(); 596 | else 597 | newIfData.failed = !negated; 598 | 599 | currentFlags.ifDatas.emplace_back(newIfData); 600 | } 601 | 602 | return std::nullopt; 603 | } 604 | 605 | std::expected CConfigImpl::parseExpression(const std::string& s) { 606 | // for now, we only support very basic expressions. 607 | // + - * / and only one per $() 608 | // TODO: something better 609 | 610 | if (s.empty()) 611 | return std::unexpected("Expression is empty"); 612 | 613 | CConstVarList args(s, 0, 's', true); 614 | 615 | if (args[1] != "+" && args[1] != "-" && args[1] != "*" && args[1] != "/") 616 | return std::unexpected("Invalid expression type: supported +, -, *, /"); 617 | 618 | auto LHS_VAR = std::ranges::find_if(variables, [&](const auto& v) { return v.name == args[0]; }); 619 | auto RHS_VAR = std::ranges::find_if(variables, [&](const auto& v) { return v.name == args[2]; }); 620 | 621 | float left = 0; 622 | float right = 0; 623 | 624 | if (LHS_VAR != variables.end()) { 625 | try { 626 | left = std::stof(LHS_VAR->value); 627 | } catch (...) { return std::unexpected("Failed to parse expression: value 1 holds a variable that does not look like a number"); } 628 | } else { 629 | try { 630 | left = std::stof(std::string{args[0]}); 631 | } catch (...) { return std::unexpected("Failed to parse expression: value 1 does not look like a number or the variable doesn't exist"); } 632 | } 633 | 634 | if (RHS_VAR != variables.end()) { 635 | try { 636 | right = std::stof(RHS_VAR->value); 637 | } catch (...) { return std::unexpected("Failed to parse expression: value 1 holds a variable that does not look like a number"); } 638 | } else { 639 | try { 640 | right = std::stof(std::string{args[2]}); 641 | } catch (...) { return std::unexpected("Failed to parse expression: value 1 does not look like a number or the variable doesn't exist"); } 642 | } 643 | 644 | switch (args[1][0]) { 645 | case '+': return left + right; 646 | case '-': return left - right; 647 | case '*': return left * right; 648 | case '/': return left / right; 649 | default: break; 650 | } 651 | 652 | return std::unexpected("Unknown error while parsing expression"); 653 | } 654 | 655 | CParseResult CConfig::parseLine(std::string line, bool dynamic) { 656 | CParseResult result; 657 | 658 | line = trim(line); 659 | 660 | auto commentPos = line.find('#'); 661 | 662 | if (commentPos == 0) { 663 | const auto COMMENT_RESULT = impl->parseComment(line.substr(1)); 664 | if (COMMENT_RESULT.has_value()) 665 | result.setError(*COMMENT_RESULT); 666 | return result; 667 | } 668 | 669 | if (!impl->currentFlags.ifDatas.empty() && impl->currentFlags.ifDatas.back().failed) 670 | return result; 671 | 672 | size_t lastHashPos = 0; 673 | 674 | while (commentPos != std::string::npos) { 675 | bool escaped = false; 676 | if (commentPos < line.length() - 1) { 677 | if (line[commentPos + 1] == '#') { 678 | lastHashPos = commentPos + 2; 679 | escaped = true; 680 | } 681 | } 682 | 683 | if (!escaped) { 684 | line = line.substr(0, commentPos); 685 | break; 686 | } else { 687 | line = line.substr(0, commentPos + 1) + line.substr(commentPos + 2); 688 | commentPos = line.find('#', lastHashPos); 689 | } 690 | } 691 | 692 | line = trim(line); 693 | 694 | if (line.empty()) 695 | return result; 696 | 697 | auto equalsPos = line.find('='); 698 | 699 | if (equalsPos == std::string::npos && !line.ends_with("{") && line != "}") { 700 | // invalid line 701 | result.setError("Invalid config line"); 702 | return result; 703 | } 704 | 705 | if (equalsPos != std::string::npos) { 706 | // set value or call handler 707 | CParseResult ret; 708 | auto LHS = trim(line.substr(0, equalsPos)); 709 | auto RHS = trim(line.substr(equalsPos + 1)); 710 | 711 | if (LHS.empty()) { 712 | result.setError("Empty lhs."); 713 | return result; 714 | } 715 | 716 | const bool ISVARIABLE = *LHS.begin() == '$'; 717 | 718 | // limit unwrapping iterations to 100. if exceeds, raise error 719 | for (size_t i = 0; i < 100; ++i) { 720 | bool anyMatch = false; 721 | 722 | // parse variables 723 | for (auto& var : impl->variables) { 724 | // don't parse LHS variables if this is a variable... 725 | const auto LHSIT = ISVARIABLE ? std::string::npos : LHS.find("$" + var.name); 726 | const auto RHSIT = RHS.find("$" + var.name); 727 | 728 | if (LHSIT != std::string::npos) 729 | replaceInString(LHS, "$" + var.name, var.value); 730 | if (RHSIT != std::string::npos) 731 | replaceInString(RHS, "$" + var.name, var.value); 732 | 733 | if (RHSIT == std::string::npos && LHSIT == std::string::npos) 734 | continue; 735 | else if (!dynamic) 736 | var.linesContainingVar.push_back({line, impl->categories, impl->currentSpecialCategory}); 737 | 738 | anyMatch = true; 739 | } 740 | 741 | // parse expressions {{somevar + 2}} 742 | // We only support single expressions for now 743 | while (RHS.contains("{{")) { 744 | auto firstUnescaped = RHS.find("{{"); 745 | // Keep searching until non-escaped expression start is found 746 | while (firstUnescaped > 0) { 747 | // Special check to avoid undefined behaviour with std::basic_string::find_last_not_of 748 | auto amountSkipped = 0; 749 | for (int i = firstUnescaped - 1; i >= 0; i--) { 750 | if (RHS.at(i) != '\\') 751 | break; 752 | amountSkipped++; 753 | } 754 | // No escape chars, or even escape chars. means they escaped themselves. 755 | if (amountSkipped % 2 == 0) 756 | break; 757 | // Continue searching for next valid expression start. 758 | firstUnescaped = RHS.find("{{", firstUnescaped + 1); 759 | // Break if the next match is never found 760 | if (firstUnescaped == std::string::npos) 761 | break; 762 | } 763 | // Real match was never found. 764 | if (firstUnescaped == std::string::npos) 765 | break; 766 | const auto BEGIN_EXPR = firstUnescaped; 767 | // "}}" doesnt need escaping. Would be invalid expression anyways. 768 | const auto END_EXPR = RHS.find("}}", BEGIN_EXPR + 2); 769 | if (END_EXPR != std::string::npos) { 770 | // try to parse the expression 771 | const auto RESULT = impl->parseExpression(RHS.substr(BEGIN_EXPR + 2, END_EXPR - BEGIN_EXPR - 2)); 772 | if (!RESULT.has_value()) { 773 | result.setError(RESULT.error()); 774 | return result; 775 | } 776 | 777 | RHS = RHS.substr(0, BEGIN_EXPR) + std::format("{}", RESULT.value()) + RHS.substr(END_EXPR + 2); 778 | } else 779 | break; 780 | } 781 | 782 | if (!anyMatch) 783 | break; 784 | 785 | if (i == 99) { 786 | result.setError("Expanding variables exceeded max iteration limit"); 787 | return result; 788 | } 789 | } 790 | 791 | if (ISVARIABLE) 792 | return parseVariable(LHS, RHS, dynamic); 793 | 794 | // Removing escape chars. -- in the future, maybe map all the chars that can be escaped. 795 | // Right now only expression parsing has escapeable chars 796 | const char ESCAPE_CHAR = '\\'; 797 | const std::array ESCAPE_SET{'{', '}'}; 798 | for (size_t i = 0; RHS.length() != 0 && i < RHS.length() - 1; i++) { 799 | if (RHS.at(i) != ESCAPE_CHAR) 800 | continue; 801 | //if escaping an escape, remove and skip the next char 802 | if (RHS.at(i + 1) == ESCAPE_CHAR) { 803 | RHS.erase(i, 1); 804 | continue; 805 | } 806 | //checks if any of the chars were escapable. 807 | for (const auto& ESCAPABLE_CHAR : ESCAPE_SET) { 808 | if (RHS.at(i + 1) != ESCAPABLE_CHAR) 809 | continue; 810 | RHS.erase(i--, 1); 811 | break; 812 | } 813 | } 814 | 815 | bool found = false; 816 | 817 | if (!impl->configOptions.verifyOnly) { 818 | auto [f, rv] = configSetValueSafe(LHS, RHS); 819 | found = f; 820 | ret = std::move(rv); 821 | ret.errorString = ret.errorStdString.c_str(); 822 | } 823 | 824 | if (!found) { 825 | for (auto& h : impl->handlers) { 826 | // we want to handle potentially nested keywords and ensure 827 | // we only call the handler if they are scoped correctly, 828 | // unless the keyword is not scoped itself 829 | 830 | const bool UNSCOPED = !h.name.contains(":"); 831 | const auto HANDLERNAME = !h.name.empty() && h.name.at(0) == ':' ? h.name.substr(1) : h.name; 832 | 833 | if (!h.options.allowFlags && !UNSCOPED) { 834 | size_t colon = 0; 835 | size_t idx = 0; 836 | size_t depth = 0; 837 | 838 | while ((colon = HANDLERNAME.find(':', idx)) != std::string::npos && impl->categories.size() > depth) { 839 | auto actual = HANDLERNAME.substr(idx, colon - idx); 840 | 841 | if (actual != impl->categories[depth]) 842 | break; 843 | 844 | idx = colon + 1; 845 | ++depth; 846 | } 847 | 848 | if (depth != impl->categories.size() || HANDLERNAME.substr(idx) != LHS) 849 | continue; 850 | } 851 | 852 | if (UNSCOPED && HANDLERNAME != LHS && !h.options.allowFlags) 853 | continue; 854 | 855 | if (h.options.allowFlags && (!LHS.starts_with(HANDLERNAME) || LHS.contains(':') /* avoid cases where a category is called the same as a handler */)) 856 | continue; 857 | 858 | ret = h.func(LHS.c_str(), RHS.c_str()); 859 | found = true; 860 | } 861 | } 862 | 863 | if (ret.error) 864 | return ret; 865 | } else { 866 | // has to be a set 867 | if (line.contains("}")) { 868 | // easiest. } or invalid. 869 | if (line != "}") { 870 | result.setError("Invalid config line"); 871 | return result; 872 | } 873 | 874 | if (impl->categories.empty()) { 875 | result.setError("Stray category close"); 876 | return result; 877 | } 878 | 879 | impl->categories.pop_back(); 880 | 881 | if (impl->categories.empty()) { 882 | impl->currentSpecialKey = ""; 883 | impl->currentSpecialCategory = nullptr; 884 | } 885 | } else { 886 | // open a category. 887 | if (!line.ends_with("{")) { 888 | result.setError("Invalid category open, garbage after {"); 889 | return result; 890 | } 891 | 892 | line.pop_back(); 893 | line = trim(line); 894 | impl->categories.push_back(line); 895 | } 896 | } 897 | 898 | return result; 899 | } 900 | 901 | CParseResult CConfig::parse() { 902 | if (!m_bCommenced) 903 | throw "Cannot parse: not commenced. You have to .commence() first."; 904 | 905 | clearState(); 906 | 907 | for (auto& [k, v] : impl->defaultValues) { 908 | impl->values.at(k).defaultFrom(v); 909 | } 910 | for (auto& sc : impl->specialCategories) { 911 | applyDefaultsToCat(*sc); 912 | } 913 | 914 | CParseResult fileParseResult; 915 | 916 | if (impl->rawConfigString.empty()) { 917 | bool fileExists = std::filesystem::exists(impl->path); 918 | 919 | // implies options.allowMissingConfig 920 | if (impl->configOptions.allowMissingConfig && !fileExists) 921 | return CParseResult{}; 922 | else if (!fileExists) { 923 | CParseResult res; 924 | res.setError("Config file is missing"); 925 | return res; 926 | } 927 | 928 | std::string canonical = std::filesystem::canonical(impl->path); 929 | 930 | fileParseResult = parseFile(canonical.c_str()); 931 | } else { 932 | fileParseResult = parseRawStream(impl->rawConfigString); 933 | } 934 | 935 | return fileParseResult; 936 | } 937 | 938 | void CConfig::changeRootPath(const char* path) { 939 | impl->path = path; 940 | } 941 | 942 | CParseResult CConfig::parseRawStream(const std::string& stream) { 943 | CParseResult result; 944 | 945 | int rawLineNum = 0; 946 | int lineNum = 0; 947 | 948 | std::stringstream str(stream); 949 | 950 | while (true) { 951 | const auto line = getNextLine(str, rawLineNum, lineNum); 952 | 953 | if (!line) { 954 | switch (line.error()) { 955 | case GETNEXTLINEFAILURE_EOF: break; 956 | case GETNEXTLINEFAILURE_BACKSLASH: 957 | if (!impl->parseError.empty()) 958 | impl->parseError += "\n"; 959 | impl->parseError += std::format("Config error: Last line ends with backslash"); 960 | result.setError(impl->parseError); 961 | break; 962 | } 963 | break; 964 | } 965 | 966 | const auto RET = parseLine(line.value()); 967 | 968 | if (RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) { 969 | if (!impl->parseError.empty()) 970 | impl->parseError += "\n"; 971 | impl->parseError += std::format("Config error at line {}: {}", lineNum, RET.errorStdString); 972 | result.setError(impl->parseError); 973 | } 974 | } 975 | 976 | if (!impl->categories.empty()) { 977 | if (impl->parseError.empty() || impl->configOptions.throwAllErrors) { 978 | if (!impl->parseError.empty()) 979 | impl->parseError += "\n"; 980 | impl->parseError += std::format("Config error: Unclosed category at EOF"); 981 | result.setError(impl->parseError); 982 | } 983 | 984 | impl->categories.clear(); 985 | } 986 | 987 | return result; 988 | } 989 | 990 | CParseResult CConfig::parseFile(const char* file) { 991 | CParseResult result; 992 | 993 | std::ifstream iffile(file); 994 | if (!iffile.good()) { 995 | result.setError("File failed to open"); 996 | return result; 997 | } 998 | 999 | int rawLineNum = 0; 1000 | int lineNum = 0; 1001 | 1002 | while (true) { 1003 | const auto line = getNextLine(iffile, rawLineNum, lineNum); 1004 | 1005 | if (!line) { 1006 | switch (line.error()) { 1007 | case GETNEXTLINEFAILURE_EOF: break; 1008 | case GETNEXTLINEFAILURE_BACKSLASH: 1009 | if (!impl->parseError.empty()) 1010 | impl->parseError += "\n"; 1011 | impl->parseError += std::format("Config error in file {}: Last line ends with backslash", file); 1012 | result.setError(impl->parseError); 1013 | break; 1014 | } 1015 | break; 1016 | } 1017 | 1018 | const auto RET = parseLine(line.value()); 1019 | 1020 | if (!impl->currentFlags.noError && RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) { 1021 | if (!impl->parseError.empty()) 1022 | impl->parseError += "\n"; 1023 | impl->parseError += std::format("Config error in file {} at line {}: {}", file, lineNum, RET.errorStdString); 1024 | result.setError(impl->parseError); 1025 | } 1026 | } 1027 | 1028 | iffile.close(); 1029 | 1030 | if (!impl->categories.empty()) { 1031 | if (impl->parseError.empty() || impl->configOptions.throwAllErrors) { 1032 | if (!impl->parseError.empty()) 1033 | impl->parseError += "\n"; 1034 | impl->parseError += std::format("Config error in file {}: Unclosed category at EOF", file); 1035 | result.setError(impl->parseError); 1036 | } 1037 | 1038 | impl->categories.clear(); 1039 | } 1040 | 1041 | return result; 1042 | } 1043 | 1044 | CParseResult CConfig::parseDynamic(const char* line) { 1045 | auto ret = parseLine(line, true); 1046 | impl->currentSpecialCategory = nullptr; 1047 | return ret; 1048 | } 1049 | 1050 | CParseResult CConfig::parseDynamic(const char* command, const char* value) { 1051 | auto ret = parseLine(std::string{command} + "=" + std::string{value}, true); 1052 | impl->currentSpecialCategory = nullptr; 1053 | return ret; 1054 | } 1055 | 1056 | void CConfig::clearState() { 1057 | impl->categories.clear(); 1058 | impl->parseError = ""; 1059 | impl->recheckEnv(); 1060 | impl->variables = impl->envVariables; 1061 | std::erase_if(impl->specialCategories, [](const auto& e) { return !e->isStatic; }); 1062 | } 1063 | 1064 | CConfigValue* CConfig::getConfigValuePtr(const char* name) { 1065 | const auto IT = impl->values.find(std::string{name}); 1066 | return IT == impl->values.end() ? nullptr : &IT->second; 1067 | } 1068 | 1069 | CConfigValue* CConfig::getSpecialConfigValuePtr(const char* category, const char* name, const char* key) { 1070 | const std::string CAT = category; 1071 | const std::string NAME = name; 1072 | const std::string KEY = key ? key : ""; 1073 | 1074 | for (auto& sc : impl->specialCategories) { 1075 | if (sc->name != CAT || (!sc->isStatic && std::string{std::any_cast(sc->values[sc->key].getValue())} != KEY)) 1076 | continue; 1077 | 1078 | const auto IT = sc->values.find(NAME); 1079 | if (IT == sc->values.end()) 1080 | return nullptr; 1081 | 1082 | return &IT->second; 1083 | } 1084 | 1085 | return nullptr; 1086 | } 1087 | 1088 | void CConfig::registerHandler(PCONFIGHANDLERFUNC func, const char* name, SHandlerOptions options_) { 1089 | SHandlerOptions options; 1090 | std::memcpy(&options, &options_, seekABIStructSize(&options_, 0, sizeof(SHandlerOptions))); 1091 | impl->handlers.push_back(SHandler{.name = name, .options = options, .func = func}); 1092 | } 1093 | 1094 | void CConfig::unregisterHandler(const char* name) { 1095 | std::erase_if(impl->handlers, [name](const auto& other) { return other.name == name; }); 1096 | } 1097 | 1098 | bool CConfig::specialCategoryExistsForKey(const char* category, const char* key) { 1099 | for (auto& sc : impl->specialCategories) { 1100 | if (sc->isStatic) 1101 | continue; 1102 | 1103 | if (sc->name != category || std::string{std::any_cast(sc->values[sc->key].getValue())} != key) 1104 | continue; 1105 | 1106 | return true; 1107 | } 1108 | 1109 | return false; 1110 | } 1111 | 1112 | /* if len != 0, out needs to be freed */ 1113 | void CConfig::retrieveKeysForCat(const char* category, const char*** out, size_t* len) { 1114 | size_t count = 0; 1115 | for (auto& sc : impl->specialCategories) { 1116 | if (sc->isStatic) 1117 | continue; 1118 | 1119 | if (sc->name != category) 1120 | continue; 1121 | 1122 | count++; 1123 | } 1124 | 1125 | if (count == 0) { 1126 | *len = 0; 1127 | return; 1128 | } 1129 | 1130 | *out = (const char**)calloc(1, count * sizeof(const char*)); 1131 | size_t counter2 = 0; 1132 | for (auto& sc : impl->specialCategories) { 1133 | if (sc->isStatic) 1134 | continue; 1135 | 1136 | if (sc->name != category) 1137 | continue; 1138 | 1139 | // EVIL, but the pointers will be almost instantly discarded by the caller 1140 | (*out)[counter2++] = (const char*)sc->values[sc->key].m_pData; 1141 | } 1142 | 1143 | *len = count; 1144 | } 1145 | --------------------------------------------------------------------------------