diff --git a/assets/config.example.json b/assets/config.example.json index 2ec03be..7b28b9a 100644 --- a/assets/config.example.json +++ b/assets/config.example.json @@ -3,6 +3,8 @@ "input_priority": "normal", "advanced_debug": false, "auto_insert": false, + "close_mic_when_idle": false, + "backend": "redux", "lock_layout": false, "clock_24h": false, "date_format": "mdy", diff --git a/src/config.cpp b/src/config.cpp index e602165..2b41b47 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -196,12 +196,22 @@ Config load_config(const std::filesystem::path& path) { if (key == "font") { if (!value.is_string) throw std::runtime_error("Config font must be a path string"); config.font = value.value; + } else if (key == "backend") { + if (!value.is_string || value.value.empty() || value.value.size() > 48 || + value.value[0] < 'a' || value.value[0] > 'z' || + !std::all_of(value.value.begin(), value.value.end(), [](unsigned char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; + })) throw std::runtime_error("Config backend must be a manifest ID"); + config.backend = value.value; } else if (key == "advanced_debug") { if (!value.is_bool) throw std::runtime_error("Config advanced_debug must be a boolean"); config.advanced_debug = value.value == "true"; } else if (key == "auto_insert") { if (!value.is_bool) throw std::runtime_error("Config auto_insert must be a boolean"); config.auto_insert = value.value == "true"; + } else if (key == "close_mic_when_idle") { + if (!value.is_bool) throw std::runtime_error("Config close_mic_when_idle must be a boolean"); + config.close_mic_when_idle = value.value == "true"; } else if (key == "lock_layout") { if (!value.is_bool) throw std::runtime_error("Config lock_layout must be a boolean"); config.lock_layout = value.value == "true"; @@ -323,6 +333,9 @@ bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexce bool save_auto_insert(const std::filesystem::path& path, bool enabled) noexcept { return save_bool_option(path, "auto_insert", enabled); } +bool save_close_mic_when_idle(const std::filesystem::path& path, bool enabled) noexcept { + return save_bool_option(path, "close_mic_when_idle", enabled); +} bool save_lock_layout(const std::filesystem::path& path, bool enabled) noexcept { return save_bool_option(path, "lock_layout", enabled); } @@ -338,6 +351,13 @@ bool save_date_format(const std::filesystem::path& path, DateFormat format) noex } return false; } +bool save_backend(const std::filesystem::path& path, const std::string& id) noexcept { + if (id.empty() || id.size() > 48 || id[0] < 'a' || id[0] > 'z' || + !std::all_of(id.begin(), id.end(), [](unsigned char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; + })) return false; + return save_option(path, "backend", id, true); +} std::string resolve_font(const std::string& assets, const std::string& requested) { const auto bundled = std::filesystem::path(assets) / "fonts/Inconsolata-Regular.ttf"; const std::array candidates{requested, bundled, diff --git a/src/config.hpp b/src/config.hpp index f9661da..b1dba9e 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -18,10 +18,12 @@ enum class DateFormat { Off, MonthDayYear, DayMonthYear, Iso }; struct Config { Theme theme; std::string font; // absolute TTF/OTF path; empty uses the bundled face + std::string backend = "redux"; // selected manifest ID; never triggers a download // Requests OpenVR's experimental global action priority; SteamVR must allow it too. bool experimental_input_priority = false; bool advanced_debug = false; // opt-in full diagnostic logging; never raw audio recording bool auto_insert = false; // opt-in; runtime also requires uninterrupted verified Xwayland focus + bool close_mic_when_idle = false; // default keeps the device open, discarding idle audio bool lock_layout = false; // hide the grab and scale handles when enabled bool clock_24h = false; DateFormat date_format = DateFormat::MonthDayYear; @@ -36,9 +38,11 @@ Config load_config(const std::filesystem::path& path); // Other user customizations and formatting are retained; creates a minimal config if absent. bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept; bool save_auto_insert(const std::filesystem::path& path, bool enabled) noexcept; +bool save_close_mic_when_idle(const std::filesystem::path& path, bool enabled) noexcept; bool save_lock_layout(const std::filesystem::path& path, bool enabled) noexcept; bool save_clock_24h(const std::filesystem::path& path, bool enabled) noexcept; bool save_date_format(const std::filesystem::path& path, DateFormat format) noexcept; +bool save_backend(const std::filesystem::path& path, const std::string& id) noexcept; std::string resolve_font(const std::string& assets, const std::string& requested); // No writes or OpenVR access when no custom button mappings are specified. // When customized, build a generated manifest and bindings under XDG cache. diff --git a/tests/config_test.cpp b/tests/config_test.cpp index 7273181..3a2749e 100644 --- a/tests/config_test.cpp +++ b/tests/config_test.cpp @@ -40,9 +40,11 @@ int main(int argc, char** argv) { ::setenv("XDG_CONFIG_HOME", dir.c_str(), 1); assert(load_config(path).buttons.empty()); assert((load_config(path).quick_inputs == std::vector{"/new", "/questions", "/help"})); + assert(load_config(path).backend == "redux"); assert(!load_config(path).experimental_input_priority); assert(!load_config(path).advanced_debug); assert(!load_config(path).auto_insert); + assert(!load_config(path).close_mic_when_idle); assert(!load_config(path).lock_layout); assert(!load_config(path).clock_24h); assert(load_config(path).date_format == DateFormat::MonthDayYear); @@ -56,10 +58,14 @@ int main(int argc, char** argv) { assert(!example.experimental_input_priority); assert(!example.advanced_debug); assert(!example.auto_insert); + assert(!example.close_mic_when_idle); assert(!example.lock_layout); assert(!example.clock_24h && example.date_format == DateFormat::MonthDayYear); assert(example.wrist.y == .18f && example.wrist.z == .089f); std::filesystem::create_directories(path.parent_path()); + assert(save_backend(path, "fixture_cpu")); + assert(load_config(path).backend == "fixture_cpu"); + assert(!save_backend(path, "../escaped") && !save_backend(path, "UPPER")); assert(save_advanced_debug(path, true)); assert(load_config(path).advanced_debug); assert(get(path).find("\"advanced_debug\":true") != std::string::npos); @@ -69,6 +75,10 @@ int main(int argc, char** argv) { assert(load_config(path).auto_insert); assert(save_auto_insert(path, false)); assert(!load_config(path).auto_insert); + assert(save_close_mic_when_idle(path, true)); + assert(load_config(path).close_mic_when_idle); + assert(save_close_mic_when_idle(path, false)); + assert(!load_config(path).close_mic_when_idle); assert(save_lock_layout(path, true)); assert(load_config(path).lock_layout); assert(save_lock_layout(path, false)); @@ -95,6 +105,11 @@ int main(int argc, char** argv) { assert(!load_config(path).advanced_debug); auto before = get(path); assert(save_advanced_debug(path, false) && get(path) == before); + for (const auto* invalid : {R"({"backend":"../escape"})", R"({"backend":""})", + R"({"backend":2})"}) { + put(path, invalid); fails([&] { load_config(path); }); + assert(!save_backend(path, "redux")); + } put(path, R"({"advanced_debug":false, "font":"kept"})"); assert(save_advanced_debug(path, true)); assert(get(path) == R"({"advanced_debug":true, "font":"kept"})"); @@ -107,6 +122,7 @@ int main(int argc, char** argv) { std::filesystem::create_symlink(path, link); assert(!save_advanced_debug(link, false)); assert(!save_auto_insert(link, true)); + assert(!save_close_mic_when_idle(link, true)); assert(!save_lock_layout(link, true)); assert(get(path) == before); std::filesystem::remove(link); @@ -128,6 +144,21 @@ int main(int argc, char** argv) { put(path, R"({"date_format":"off","font":"kept"})"); assert(save_date_format(path, DateFormat::Iso)); assert(get(path) == R"({"date_format":"iso","font":"kept"})"); + for (const auto* invalid : {R"({"close_mic_when_idle":"true"})", R"({"close_mic_when_idle":1})", + R"({"close_mic_when_idle":null})", R"({"close_mic_when_idle":[]})"}) { + put(path, invalid); + fails([&] { load_config(path); }); + assert(!save_close_mic_when_idle(path, true)); + assert(get(path) == invalid); + } + put(path, R"({"close_mic_when_idle":false,"font":"kept"})"); + assert(save_close_mic_when_idle(path, true)); + assert(load_config(path).close_mic_when_idle); + assert(get(path) == R"({"close_mic_when_idle":true,"font":"kept"})"); + before = get(path); + assert(save_close_mic_when_idle(path, true) && get(path) == before); + assert(save_close_mic_when_idle(path, false)); + assert(get(path) == R"({"close_mic_when_idle":false,"font":"kept"})"); put(path, R"({"auto_insert":"on"})"); fails([&] { load_config(path); }); assert(!save_auto_insert(path, true));