diff --git a/CMakeLists.txt b/CMakeLists.txt index ed0548b..37c330f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.19) -set(HYPRLANG_VERSION "0.5.0") +set(HYPRLANG_VERSION "0.5.1") project(hyprlang VERSION ${HYPRLANG_VERSION} diff --git a/src/VarList.cpp b/src/VarList.cpp new file mode 100644 index 0000000..518acde --- /dev/null +++ b/src/VarList.cpp @@ -0,0 +1,55 @@ +#include "VarList.hpp" +#include +#include + +static std::string removeBeginEndSpacesTabs(std::string str) { + if (str.empty()) + return str; + + int countBefore = 0; + while (str[countBefore] == ' ' || str[countBefore] == '\t') { + countBefore++; + } + + int countAfter = 0; + while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) { + countAfter++; + } + + str = str.substr(countBefore, str.length() - countBefore - countAfter); + + return str; +} + +CVarList::CVarList(const std::string& in, const size_t lastArgNo, const char delim, const bool removeEmpty) { + if (in.empty()) + m_vArgs.emplace_back(""); + + std::string args{in}; + size_t idx = 0; + size_t pos = 0; + std::ranges::replace_if( + args, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0); + + for (const auto& s : args | std::views::split(0)) { + if (removeEmpty && s.empty()) + continue; + if (++idx == lastArgNo) { + m_vArgs.emplace_back(removeBeginEndSpacesTabs(in.substr(pos))); + break; + } + pos += s.size() + 1; + m_vArgs.emplace_back(removeBeginEndSpacesTabs(std::string_view{s}.data())); + } +} + +std::string CVarList::join(const std::string& joiner, size_t from, size_t to) const { + size_t last = to == 0 ? size() : to; + + std::string rolling; + for (size_t i = from; i < last; ++i) { + rolling += m_vArgs[i] + (i + 1 < last ? joiner : ""); + } + + return rolling; +} \ No newline at end of file diff --git a/src/VarList.hpp b/src/VarList.hpp new file mode 100644 index 0000000..1374da6 --- /dev/null +++ b/src/VarList.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include + +class CVarList { + public: + /** Split string into arg list + @param lastArgNo stop splitting after argv reaches maximum size, last arg will contain rest of unsplit args + @param delim if delimiter is 's', use std::isspace + @param removeEmpty remove empty args from argv + */ + CVarList(const std::string& in, const size_t maxSize = 0, const char delim = ',', const bool removeEmpty = false); + + ~CVarList() = default; + + size_t size() const { + return m_vArgs.size(); + } + + std::string join(const std::string& joiner, size_t from = 0, size_t to = 0) const; + + void map(std::function func) { + for (auto& s : m_vArgs) + func(s); + } + + void append(const std::string arg) { + m_vArgs.emplace_back(arg); + } + + std::string operator[](const size_t& idx) const { + if (idx >= m_vArgs.size()) + return ""; + return m_vArgs[idx]; + } + + // for range-based loops + std::vector::iterator begin() { + return m_vArgs.begin(); + } + std::vector::const_iterator begin() const { + return m_vArgs.begin(); + } + std::vector::iterator end() { + return m_vArgs.end(); + } + std::vector::const_iterator end() const { + return m_vArgs.end(); + } + + bool contains(const std::string& el) { + for (auto& a : m_vArgs) { + if (a == el) + return true; + } + + return false; + } + + private: + std::vector m_vArgs; +}; \ No newline at end of file diff --git a/src/config.cpp b/src/config.cpp index bfdffc1..bee4e64 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -9,8 +9,16 @@ #include #include +#include "VarList.hpp" + using namespace Hyprlang; + +#ifdef __APPLE__ +#include +#define environ (*_NSGetEnviron()) +#else extern "C" char** environ; +#endif // defines inline constexpr const char* ANONYMOUS_KEY = "__hyprlang_internal_anonymous_key"; @@ -295,7 +303,37 @@ CParseResult CConfig::configSetValueSafe(const std::string& command, const std:: const auto VALUEONLYNAME = command.starts_with(catPrefix) ? command.substr(catPrefix.length()) : command; - auto VALUEIT = impl->values.find(valueName); + // FIXME: this will bug with nested. + if (valueName.contains('[') && valueName.contains(']')) { + const auto L = valueName.find_first_of('['); + const auto R = valueName.find_last_of(']'); + + if (L < R) { + const auto CATKEY = valueName.substr(L + 1, R - L - 1); + impl->currentSpecialKey = CATKEY; + + valueName = valueName.substr(0, L) + valueName.substr(R + 1); + + // if it doesn't exist, make it + for (auto& sc : impl->specialCategoryDescriptors) { + if (sc->key.empty() || !valueName.starts_with(sc->name)) + continue; + + // bingo + const auto PCAT = impl->specialCategories.emplace_back(std::make_unique()).get(); + PCAT->descriptor = sc.get(); + PCAT->name = sc->name; + PCAT->key = sc->key; + addSpecialConfigValue(sc->name.c_str(), sc->key.c_str(), CConfigValue(CATKEY.c_str())); + + applyDefaultsToCat(*PCAT); + + PCAT->values[sc->key].setFrom(CATKEY); + } + } + } + + auto VALUEIT = impl->values.find(valueName); if (VALUEIT == impl->values.end()) { // it might be in a special category bool found = false; @@ -474,11 +512,31 @@ CParseResult CConfig::parseVariable(const std::string& lhs, const std::string& r return result; } +void CConfigImpl::parseComment(const std::string& comment) { + const auto COMMENT = removeBeginEndSpacesTabs(comment); + + if (!COMMENT.starts_with("hyprlang")) + return; + + CVarList args(COMMENT, 0, 's', true); + + if (args[1] == "noerror") + currentFlags.noError = args[2] == "true" || args[2] == "yes" || args[2] == "enable" || args[2] == "enabled" || args[2] == "set"; +} + CParseResult CConfig::parseLine(std::string line, bool dynamic) { CParseResult result; - auto commentPos = line.find('#'); - size_t lastHashPos = 0; + line = removeBeginEndSpacesTabs(line); + + auto commentPos = line.find('#'); + + if (commentPos == 0) { + impl->parseComment(line.substr(1)); + return result; + } + + size_t lastHashPos = 0; while (commentPos != std::string::npos) { bool escaped = false; @@ -500,9 +558,12 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) { line = removeBeginEndSpacesTabs(line); + if (line.empty()) + return result; + auto equalsPos = line.find('='); - if (equalsPos == std::string::npos && !line.ends_with("{") && line != "}" && !line.empty()) { + if (equalsPos == std::string::npos && !line.ends_with("{") && line != "}") { // invalid line result.setError("Invalid config line"); return result; @@ -519,14 +580,14 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) { return result; } - if (*LHS.begin() == '$') - return parseVariable(LHS, RHS, dynamic); + const bool ISVARIABLE = *LHS.begin() == '$'; // limit unwrapping iterations to 100. if exceeds, raise error for (size_t i = 0; i < 100; ++i) { bool anyMatch = false; for (auto& var : impl->variables) { - const auto LHSIT = LHS.find("$" + var.name); + // don't parse LHS variables if this is a variable... + const auto LHSIT = ISVARIABLE ? std::string::npos : LHS.find("$" + var.name); const auto RHSIT = RHS.find("$" + var.name); if (LHSIT != std::string::npos) @@ -551,6 +612,9 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) { } } + if (ISVARIABLE) + return parseVariable(LHS, RHS, dynamic); + bool found = false; for (auto& h : impl->handlers) { if (!h.options.allowFlags && h.name != LHS) @@ -568,7 +632,7 @@ CParseResult CConfig::parseLine(std::string line, bool dynamic) { if (ret.error) return ret; - } else if (!line.empty()) { + } else { // has to be a set if (line.contains("}")) { // easiest. } or invalid. @@ -689,7 +753,7 @@ CParseResult CConfig::parseFile(const char* file) { const auto RET = parseLine(line); - if (RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) { + if (!impl->currentFlags.noError && RET.error && (impl->parseError.empty() || impl->configOptions.throwAllErrors)) { if (!impl->parseError.empty()) impl->parseError += "\n"; impl->parseError += std::format("Config error in file {} at line {}: {}", file, linenum, RET.errorStdString); diff --git a/src/config.hpp b/src/config.hpp index 80d21b1..54674a1 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -87,4 +87,10 @@ class CConfigImpl { std::string parseError = ""; Hyprlang::SConfigOptions configOptions; + + void parseComment(const std::string& comment); + + struct { + bool noError = false; + } currentFlags; }; \ No newline at end of file diff --git a/tests/config/config.conf b/tests/config/config.conf index e0f564f..1feedd1 100644 --- a/tests/config/config.conf +++ b/tests/config/config.conf @@ -1,5 +1,10 @@ # Test comment +## This is also a comment + ## This is a comment with space as a first character + ## This is a comment with tab as a first character + ## This is a comment with leading spaces and tabs + ##### Comment with more hash tags testInt = 123 testFloat = 123.456 @@ -17,6 +22,12 @@ customType = abc testStringColon = ee:ee:ee +# hyprlang noerror true + +errorVariable = true + +# hyprlang noerror false + testCategory { testValueInt = 123456 testValueHex = 0xF @@ -40,8 +51,7 @@ special { value = $SPECIALVAL1 } -special { - key = b +special[b] { value = 2 } @@ -67,6 +77,10 @@ specialAnonymous { testCategory:testValueHex = 0xFFfFaAbB +$RECURSIVE1 = a +$RECURSIVE2 = $RECURSIVE1b +testStringRecursive = $RECURSIVE2c + testStringQuotes = "Hello World!" #testDefault = 123 diff --git a/tests/parse/main.cpp b/tests/parse/main.cpp index ef7524f..359757a 100644 --- a/tests/parse/main.cpp +++ b/tests/parse/main.cpp @@ -89,6 +89,7 @@ int main(int argc, char** argv, char** envp) { config.addConfigValue("testEnv", ""); config.addConfigValue("testVar", (Hyprlang::INT)0); config.addConfigValue("testStringQuotes", ""); + config.addConfigValue("testStringRecursive", ""); config.addConfigValue("testCategory:testValueInt", (Hyprlang::INT)0); config.addConfigValue("testCategory:testValueHex", (Hyprlang::INT)0xA); config.addConfigValue("testCategory:nested1:testValueNest", (Hyprlang::INT)0); @@ -178,12 +179,16 @@ int main(int argc, char** argv, char** envp) { // test variables std::cout << " → Testing variables\n"; EXPECT(std::any_cast(config.getConfigValue("testVar")), 13371337); + EXPECT(std::any_cast(config.getConfigValue("testStringRecursive")), std::string{"abc"}); // test dynamic variables std::cout << " → Testing dynamic variables\n"; EXPECT(config.parseDynamic("$MY_VAR_2 = 420").error, false); EXPECT(std::any_cast(config.getConfigValue("testVar")), 1337420); + EXPECT(config.parseDynamic("$RECURSIVE1 = d").error, false); + EXPECT(std::any_cast(config.getConfigValue("testStringRecursive")), std::string{"dbc"}); + // test env variables std::cout << " → Testing env variables\n"; EXPECT(std::any_cast(config.getConfigValue("testEnv")), std::string{getenv("SHELL")}); @@ -194,6 +199,8 @@ int main(int argc, char** argv, char** envp) { EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "b")), 2); EXPECT(std::any_cast(config.getSpecialConfigValue("specialGeneric:one", "value")), 1); EXPECT(std::any_cast(config.getSpecialConfigValue("specialGeneric:two", "value")), 2); + EXPECT(config.parseDynamic("special[b]:value = 3").error, false); + EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "b")), 3); // test dynamic special variable EXPECT(config.parseDynamic("$SPECIALVAL1 = 2").error, false);