From 10a11510ba9913cd6edd7855f1e2dd6f1c6a812c Mon Sep 17 00:00:00 2001 From: baketnk Date: Thu, 24 Sep 2026 16:44:32 -0400 Subject: [PATCH] Show local clock and configurable date in panel header --- README.md | 3 +- assets/config.example.json | 2 ++ docs/overlay.md | 14 +++++++++ install.sh | 6 ++++ scripts/install_payload.py | 6 ++++ src/config.cpp | 36 +++++++++++++++++++--- src/config.hpp | 5 +++ src/overlay.cpp | 20 +++++++++++- src/panel_clock.hpp | 27 +++++++++++++++++ src/panel_surface.cpp | 62 ++++++++++++++++++++++++++------------ src/panel_surface.hpp | 6 ++++ tests/config_test.cpp | 31 +++++++++++++++++++ tests/panel_test.cpp | 22 +++++++++++++- tests/test_installer.py | 6 ++++ 14 files changed, 218 insertions(+), 28 deletions(-) create mode 100644 src/panel_clock.hpp diff --git a/README.md b/README.md index 6ef58ba..ef42e99 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet **Left grip (when active):** double-tap for the same explicit Enter action. Nothing submits automatically. Auto Insert is opt-in and off by default. - **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit. - Review and settings share one Inconsolata/neon-framed surface. + Review and settings share one Inconsolata/neon-framed surface. The header + shows local time/date; Settings selects 12/24-hour time and date format/off. Drag the thin bar below the panel to move it in its plane; drag the external lower-right corner bracket to scale it (world, head or wrist). Both sit in transparent margins, like Steam's window handles. Scaling keeps the upper-left diff --git a/assets/config.example.json b/assets/config.example.json index 1137d3d..fb8520b 100644 --- a/assets/config.example.json +++ b/assets/config.example.json @@ -3,6 +3,8 @@ "input_priority": "normal", "advanced_debug": false, "auto_insert": false, + "clock_24h": false, + "date_format": "mdy", "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "theme": { "background": "#0c101b", diff --git a/docs/overlay.md b/docs/overlay.md index 8dce85a..22569d6 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -51,6 +51,12 @@ requires an on-device comparison. The [Vulkan deployment record](evidence/vulkan-overlay-2026-09-24.md) documents the native installation and offscreen GPU checks separately from headset acceptance. +The header shows local time and date instead of the former on-device/review +and current-mount labels. It updates when the displayed minute or date changes, +not every frame. Settings toggles 12/24-hour time and cycles date Off → +MM/DD/YYYY → DD/MM/YYYY → YYYY-MM-DD → Off. These only affect display; +mount choices remain in Settings. + The complete transcript preview is paginated by glyph width and four-line height; Previous and Next navigate it without changing the source transcript. Long status/detail messages show a prefix with a visible truncation marker. @@ -99,6 +105,8 @@ installer creates one with defaults on first install. Copy the shipped "input_priority": "normal", "advanced_debug": false, "auto_insert": false, + "clock_24h": false, + "date_format": "mdy", "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "theme": { "background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9", @@ -116,6 +124,12 @@ installer creates one with defaults on first install. Copy the shipped } ``` +`clock_24h` is a boolean (default `false`); `date_format` is `off`, `mdy` +(default), `dmy`, or `iso`. The Settings buttons update these preferences +immediately and save them to the user config when persistence is enabled. +A failed save warns and leaves the selection active for this run. Time uses +the device's local timezone; these controls do not change system time. + Each theme color is `#RRGGBB`; omitted colors keep the default. `font` is a TTF/OTF file path (not a family name); a missing file uses the bundled font. `advanced_debug` is a boolean (default `false`, not a string): an opt-in diff --git a/install.sh b/install.sh index 7f6ae4b..8b4b87a 100755 --- a/install.sh +++ b/install.sh @@ -36,6 +36,8 @@ CONFIG_DEFAULTS = { "input_priority": "normal", "advanced_debug": False, "auto_insert": False, + "clock_24h": False, + "date_format": "mdy", "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9", "muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87", @@ -98,6 +100,10 @@ def normalized_config(data): fixed["advanced_debug"] = debug if type(debug) is bool else False automatic = data.get("auto_insert", False) fixed["auto_insert"] = automatic if type(automatic) is bool else False + clock = data.get("clock_24h", False) + fixed["clock_24h"] = clock if type(clock) is bool else False + date = data.get("date_format", "mdy") + fixed["date_format"] = date if date in ("off", "mdy", "dmy", "iso") else "mdy" source = data.get("wrist") source = source if isinstance(source, dict) else {} fixed["wrist"] = {} diff --git a/scripts/install_payload.py b/scripts/install_payload.py index b6c9ad5..2b2fcf0 100644 --- a/scripts/install_payload.py +++ b/scripts/install_payload.py @@ -25,6 +25,8 @@ CONFIG_DEFAULTS = { "input_priority": "normal", "advanced_debug": False, "auto_insert": False, + "clock_24h": False, + "date_format": "mdy", "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9", "muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87", @@ -87,6 +89,10 @@ def normalized_config(data): fixed["advanced_debug"] = debug if type(debug) is bool else False automatic = data.get("auto_insert", False) fixed["auto_insert"] = automatic if type(automatic) is bool else False + clock = data.get("clock_24h", False) + fixed["clock_24h"] = clock if type(clock) is bool else False + date = data.get("date_format", "mdy") + fixed["date_format"] = date if date in ("off", "mdy", "dmy", "iso") else "mdy" source = data.get("wrist") source = source if isinstance(source, dict) else {} fixed["wrist"] = {} diff --git a/src/config.cpp b/src/config.cpp index 7a215e6..e4f1103 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -200,6 +200,16 @@ Config load_config(const std::filesystem::path& path) { } 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 == "clock_24h") { + if (!value.is_bool) throw std::runtime_error("Config clock_24h must be a boolean"); + config.clock_24h = value.value == "true"; + } else if (key == "date_format") { + if (!value.is_string) throw std::runtime_error("Config date_format must be a string"); + if (value.value == "off") config.date_format = DateFormat::Off; + else if (value.value == "mdy") config.date_format = DateFormat::MonthDayYear; + else if (value.value == "dmy") config.date_format = DateFormat::DayMonthYear; + else if (value.value == "iso") config.date_format = DateFormat::Iso; + else throw std::runtime_error("Config date_format must be off, mdy, dmy or iso"); } else if (key == "input_priority") { if (!value.is_string || (value.value != "normal" && value.value != "experimental")) throw std::runtime_error("Config input_priority must be normal or experimental"); @@ -254,7 +264,8 @@ Config load_config(const std::filesystem::path& path) { return config; } namespace { -bool save_bool_option(const std::filesystem::path& path, std::string_view key, bool enabled) noexcept { +bool save_option(const std::filesystem::path& path, std::string_view key, + std::string_view value, bool string_value) noexcept { try { if (path.empty() || !path.is_absolute() || std::filesystem::is_symlink(path)) return false; const bool existing = std::filesystem::exists(path); @@ -264,15 +275,15 @@ bool save_bool_option(const std::filesystem::path& path, std::string_view key, b Parser parser{bytes}; auto root = parser.parse(); parser.ws(); if (!root.is_object || parser.pos != bytes.size()) return false; - const std::string value = enabled ? "true" : "false"; + const std::string encoded = string_value ? "\"" + std::string(value) + "\"" : std::string(value); auto it = root.object.find(std::string(key)); if (it != root.object.end()) { - if (!it->second.is_bool) return false; + if (it->second.is_string != string_value || (!string_value && !it->second.is_bool)) return false; if (it->second.value == value) return true; - bytes.replace(it->second.start, it->second.end - it->second.start, value); + bytes.replace(it->second.start, it->second.end - it->second.start, encoded); } else { bytes.insert(root.end - 1, std::string(root.object.empty() ? "" : ",") + - "\"" + std::string(key) + "\":" + value); + "\"" + std::string(key) + "\":" + encoded); } // The native reader rejects files >=4097 bytes, even if the JSON is valid. if (bytes.size() > 4096) return false; @@ -285,6 +296,9 @@ bool save_bool_option(const std::filesystem::path& path, std::string_view key, b return false; } } +bool save_bool_option(const std::filesystem::path& path, std::string_view key, bool enabled) noexcept { + return save_option(path, key, enabled ? "true" : "false", false); +} } // namespace bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept { return save_bool_option(path, "advanced_debug", enabled); @@ -292,6 +306,18 @@ 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_clock_24h(const std::filesystem::path& path, bool enabled) noexcept { + return save_bool_option(path, "clock_24h", enabled); +} +bool save_date_format(const std::filesystem::path& path, DateFormat format) noexcept { + switch (format) { + case DateFormat::Off: return save_option(path, "date_format", "off", true); + case DateFormat::MonthDayYear: return save_option(path, "date_format", "mdy", true); + case DateFormat::DayMonthYear: return save_option(path, "date_format", "dmy", true); + case DateFormat::Iso: return save_option(path, "date_format", "iso", true); + } + return false; +} 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 8272c69..8cd6022 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -13,6 +13,7 @@ struct Theme { Rgba accent{31, 240, 164, 255}, warning{255, 110, 135, 255}; Rgba frame_start{31, 255, 145, 255}, frame_end{31, 112, 255, 255}; }; +enum class DateFormat { Off, MonthDayYear, DayMonthYear, Iso }; struct Config { Theme theme; std::string font; // absolute TTF/OTF path; empty uses the bundled face @@ -20,6 +21,8 @@ struct Config { 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 clock_24h = false; + DateFormat date_format = DateFormat::MonthDayYear; WristPlacement wrist; // OpenVR action name -> physical Frame controller input path; empty disables it. std::map buttons; @@ -30,6 +33,8 @@ 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_clock_24h(const std::filesystem::path& path, bool enabled) noexcept; +bool save_date_format(const std::filesystem::path& path, DateFormat format) 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/src/overlay.cpp b/src/overlay.cpp index 4267043..e2f7be2 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -179,6 +180,8 @@ struct Overlay::Impl { surface.set_lasers_anytime(lasers_anytime); surface.set_advanced_debug(config.advanced_debug); surface.set_auto_insert(config.auto_insert); + surface.set_clock_24h(config.clock_24h); + surface.set_date_format(config.date_format); overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard"); vr::HmdVector2_t mouse_scale{{float(W), float(H)}}; overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale"); @@ -360,6 +363,7 @@ struct Overlay::Impl { bool available(UiAction action) const { return surface.available(action); } void draw(const Panel& p) { panel = p; + surface.set_clock_time(std::time(nullptr)); if (surface.render(p)) { gpu_texture->upload(surface.pixels()); auto texture = gpu_texture->texture(); @@ -473,8 +477,22 @@ struct Overlay::Impl { last_pointer_event = "up button=" + std::to_string(event.data.mouse.button); if (event.data.mouse.button == vr::VRMouseButton_Left) { auto event_result = surface.pointer_up(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y); - if (event_result.action || event_result.mount || event_result.recenter || event_result.lasers_anytime || event_result.open_bindings || event_result.advanced_debug || event_result.auto_insert) ++pointer_actions; + if (event_result.action || event_result.mount || event_result.recenter || event_result.lasers_anytime || event_result.open_bindings || event_result.advanced_debug || event_result.auto_insert || event_result.clock_24h || event_result.date_format) ++pointer_actions; if (event_result.action) result.push_back(*event_result.action); + if (event_result.clock_24h) { + config.clock_24h = *event_result.clock_24h; + save_failed = persist_mount && !save_clock_24h(default_config_path(), config.clock_24h); + surface.set_clock_24h(config.clock_24h); + reset_input(result); + return result; + } + if (event_result.date_format) { + config.date_format = *event_result.date_format; + save_failed = persist_mount && !save_date_format(default_config_path(), config.date_format); + surface.set_date_format(config.date_format); + reset_input(result); + return result; + } if (event_result.auto_insert) { config.auto_insert = *event_result.auto_insert; auto_save_failed = persist_mount && !save_auto_insert(default_config_path(), config.auto_insert); diff --git a/src/panel_clock.hpp b/src/panel_clock.hpp new file mode 100644 index 0000000..27ea287 --- /dev/null +++ b/src/panel_clock.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "config.hpp" +#include +#include +#include + +namespace frameyap { +struct ClockLabel { std::string time, date; }; +// Local wall time; caller refreshes on minute/day change, never by forcing +// a continuous redraw. No OpenVR or system-clock changes. +inline ClockLabel panel_clock(std::time_t now, bool clock_24h, DateFormat format) { + std::tm local{}; + if (!localtime_r(&now, &local)) return {"--:--", {}}; + std::array out{}; + auto format_part = [&](const char* pattern) { + return std::strftime(out.data(), out.size(), pattern, &local) ? std::string(out.data()) : std::string(); + }; + ClockLabel result{format_part(clock_24h ? "%H:%M" : "%I:%M %p"), {}}; + switch (format) { + case DateFormat::Off: break; + case DateFormat::MonthDayYear: result.date = format_part("%m/%d/%Y"); break; + case DateFormat::DayMonthYear: result.date = format_part("%d/%m/%Y"); break; + case DateFormat::Iso: result.date = format_part("%Y-%m-%d"); break; + } + return result; +} +} // namespace frameyap diff --git a/src/panel_surface.cpp b/src/panel_surface.cpp index 8702267..1efcd88 100644 --- a/src/panel_surface.cpp +++ b/src/panel_surface.cpp @@ -1,4 +1,5 @@ #include "panel_surface.hpp" +#include "panel_clock.hpp" #include #include FT_FREETYPE_H #include @@ -28,10 +29,11 @@ struct Rect { } }; enum class Control { Review, Settings, Bindings, Prev, Next, Record, Cancel, Insert, Enter, Quit, - World, Left, Right, Head, Recenter, LasersAnytime, AdvancedDebug, AutoInsert }; + World, Left, Right, Head, Recenter, LasersAnytime, AdvancedDebug, AutoInsert, + Clock24h, Date }; enum class Tab { Review, Settings }; struct Button { Rect r; Control id; const char* label; }; -constexpr std::array buttons{{ +constexpr std::array buttons{{ {{32, 138, 180, 46}, Control::Review, "Review"}, {{226, 138, 180, 46}, Control::Settings, "Settings"}, {{420, 138, 180, 46}, Control::Bindings, "Bindings"}, @@ -50,6 +52,8 @@ constexpr std::array buttons{{ {{514, 394, 454, 50}, Control::LasersAnytime, "Lasers anytime"}, {{32, 452, 454, 48}, Control::AutoInsert, "Auto insert"}, {{514, 452, 454, 48}, Control::AdvancedDebug, "Advanced debug"}, + {{32, 506, 454, 42}, Control::Clock24h, "Clock"}, + {{514, 506, 454, 42}, Control::Date, "Date"}, }}; std::optional action(Control c) { switch (c) { @@ -70,15 +74,6 @@ std::optional mounting(Control c) { default: return {}; } } -std::string_view mount_label(Mount m) { - switch (m) { - case Mount::World: return "World space"; - case Mount::LeftWrist: return "Left wrist"; - case Mount::RightWrist: return "Right wrist"; - case Mount::Head: return "Head"; - } - return "World space"; -} // Invalid bytes become visible replacement glyphs, never control commands. uint32_t next_codepoint(std::string_view s, size_t& i) { const auto a = static_cast(s[i++]); @@ -109,6 +104,10 @@ struct PanelSurface::Impl { Color background, card, ink, muted, cyan, pink; Tab tab = Tab::Review; bool dirty = true, lasers_anytime = false, advanced_debug = false, auto_insert = false; + bool clock_24h = false; + DateFormat date_format = DateFormat::MonthDayYear; + std::time_t clock_time = std::time(nullptr); + ClockLabel displayed_clock; std::string placement_note, binding_note; std::array pressed{{-1, -1}}; int drag_cursor = -1; @@ -254,7 +253,8 @@ struct PanelSurface::Impl { bool visible(Control c) const { if (c == Control::Prev || c == Control::Next) return tab == Tab::Review; if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime || - c == Control::AdvancedDebug || c == Control::AutoInsert) return tab == Tab::Settings; + c == Control::AdvancedDebug || c == Control::AutoInsert || + c == Control::Clock24h || c == Control::Date) return tab == Tab::Settings; return true; } bool enabled(Control c) const { @@ -286,13 +286,16 @@ struct PanelSurface::Impl { if (p.status != panel.status || p.detail != panel.detail || p.enabled != panel.enabled || p.recording != panel.recording) dirty = true; panel = p; + auto label = panel_clock(clock_time, clock_24h, date_format); + if (label.time != displayed_clock.time || label.date != displayed_clock.date) dirty = true; if (!dirty) return false; + displayed_clock = std::move(label); std::fill(pixels.begin(), pixels.end(), 0); rect({0, 0, CW, CH}, background); frame(); text("FrameYap", 32, 61, 40, ink, 300); - text(auto_insert ? "ON-DEVICE / AUTO INSERT OPT-IN" : "ON-DEVICE / REVIEW FIRST", 280, 58, 22, muted, 720); - text(mount_label(mount), 756, 58, 24, cyan, 968); + text(displayed_clock.time, 475, 58, 28, ink, 735); + if (!displayed_clock.date.empty()) text(displayed_clock.date, 756, 58, 24, cyan, 968); rounded({32, 78, 936, 48}, 13, mix(background, card, .7f), panel.recording ? mix(card, pink, .36f) : mix(card, cyan, .22f), panel.recording ? .16f : 0.f); @@ -313,10 +316,9 @@ struct PanelSurface::Impl { if (detail.size() > 2) text("[detail truncated]", 730, 546, 22, pink, 968); if (!binding_note.empty()) text(binding_note, 32, 203, 20, muted, 968); } else { - text("MOUNT AND INTERACTION", 32, 224, 22, muted, 968); - text("Auto insert requires stable X focus; never Enter. Debug logs may contain speech.", 32, 520, 20, pink, 968); - text(placement_note.empty() ? "Debug restarts worker; lasers may affect games. No saved audio clips." : placement_note, - 32, 540, 20, muted, 968); + text("Auto insert needs stable X focus; debug logs may contain speech.", 32, 204, 18, pink, 968); + text(placement_note.empty() ? "No automatic Enter; changing debug restarts the worker." : placement_note, + 32, 225, 18, muted, 968); } rect({32, 550, 936, 1}, mix(card, cyan, .17f)); for (size_t i = 0; i < buttons.size(); ++i) { @@ -337,8 +339,13 @@ struct PanelSurface::Impl { rounded(b.r, std::min(16, b.r.h / 3), fill, !on ? mix(card, muted, .13f) : highlighted ? accent : mix(card, muted, .38f), highlighted ? .23f : 0.f, highlighted ? 2 : 1); - const auto label = b.id == Control::Record && panel.recording ? "Stop" : b.label; - text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, 27, on ? ink : mix(background, muted, .48f), b.r.x + b.r.w - 8); + const std::string label = b.id == Control::Record && panel.recording ? "Stop" : + b.id == Control::Clock24h ? (clock_24h ? "Clock: 24 hour" : "Clock: 12 hour") : + b.id == Control::Date ? (date_format == DateFormat::Off ? "Date: Off" : + date_format == DateFormat::MonthDayYear ? "Date: MM/DD/YYYY" : + date_format == DateFormat::DayMonthYear ? "Date: DD/MM/YYYY" : "Date: YYYY-MM-DD") : b.label; + text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, b.id == Control::Date ? 23 : 27, + on ? ink : mix(background, muted, .48f), b.r.x + b.r.w - 8); if (mounting(b.id) && selected) text("ON", b.r.x + b.r.w - 56, b.r.y + 38, 23, cyan, b.r.x + b.r.w - 12); if (b.id == Control::LasersAnytime || b.id == Control::AdvancedDebug || b.id == Control::AutoInsert) { bool active = b.id == Control::LasersAnytime ? lasers_anytime : @@ -405,6 +412,8 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) { else if (c == Control::LasersAnytime) result.lasers_anytime = !impl_->lasers_anytime; else if (c == Control::AdvancedDebug) result.advanced_debug = !impl_->advanced_debug; else if (c == Control::AutoInsert) result.auto_insert = !impl_->auto_insert; + else if (c == Control::Clock24h) result.clock_24h = !impl_->clock_24h; + else if (c == Control::Date) result.date_format = static_cast((static_cast(impl_->date_format) + 1) % 4); else if (c == Control::Bindings) { result.open_bindings = true; impl_->reset(); } else if (c == Control::Review || c == Control::Settings) { impl_->tab = c == Control::Review ? Tab::Review : Tab::Settings; @@ -442,4 +451,17 @@ void PanelSurface::set_auto_insert(bool enabled) { impl_->dirty = true; } } +void PanelSurface::set_clock_24h(bool enabled) { + if (impl_->clock_24h != enabled) { + impl_->clock_24h = enabled; impl_->reset(); impl_->dirty = true; + } +} +void PanelSurface::set_date_format(DateFormat format) { + if (impl_->date_format != format) { + impl_->date_format = format; impl_->reset(); impl_->dirty = true; + } +} +void PanelSurface::set_clock_time(std::time_t now) { + impl_->clock_time = now; +} } // namespace frameyap diff --git a/src/panel_surface.hpp b/src/panel_surface.hpp index 6ccabb6..5d57dfd 100644 --- a/src/panel_surface.hpp +++ b/src/panel_surface.hpp @@ -2,6 +2,7 @@ #include "overlay.hpp" #include "config.hpp" #include "panel_drag.hpp" +#include #include #include #include @@ -14,6 +15,8 @@ struct SurfaceEvent { std::optional lasers_anytime; std::optional advanced_debug; std::optional auto_insert; + std::optional clock_24h; + std::optional date_format; bool recenter = false; bool open_bindings = false; }; @@ -41,6 +44,9 @@ public: void set_lasers_anytime(bool enabled); void set_advanced_debug(bool enabled); void set_auto_insert(bool enabled); + void set_clock_24h(bool enabled); + void set_date_format(DateFormat format); + void set_clock_time(std::time_t now); void set_binding_note(std::string note); bool available(UiAction action) const; private: diff --git a/tests/config_test.cpp b/tests/config_test.cpp index def3fa4..cb3f2be 100644 --- a/tests/config_test.cpp +++ b/tests/config_test.cpp @@ -1,4 +1,5 @@ #include "config.hpp" +#include "panel_clock.hpp" #include #include #include @@ -14,6 +15,17 @@ template void fails(Fn fn) { bool raised = false; try { fn(); } catch } int main(int argc, char** argv) { assert(argc == 2); + ::setenv("TZ", "UTC", 1); tzset(); + std::tm instant{}; + instant.tm_year = 124; instant.tm_mon = 0; instant.tm_mday = 2; + instant.tm_hour = 15; instant.tm_min = 4; + auto at = std::mktime(&instant); + assert(panel_clock(at, false, DateFormat::MonthDayYear).time == "03:04 PM"); + assert(panel_clock(at, false, DateFormat::MonthDayYear).date == "01/02/2024"); + assert(panel_clock(at, true, DateFormat::DayMonthYear).time == "15:04"); + assert(panel_clock(at, true, DateFormat::DayMonthYear).date == "02/01/2024"); + assert(panel_clock(at, true, DateFormat::Iso).date == "2024-01-02"); + assert(panel_clock(at, true, DateFormat::Off).date.empty()); const auto dir = std::filesystem::temp_directory_path() / ("frameyap-config-test-" + std::to_string(::getpid())); std::filesystem::create_directories(dir); ::setenv("XDG_CONFIG_HOME", dir.c_str(), 1); @@ -30,6 +42,8 @@ int main(int argc, char** argv) { assert(!load_config(path).experimental_input_priority); assert(!load_config(path).advanced_debug); assert(!load_config(path).auto_insert); + assert(!load_config(path).clock_24h); + assert(load_config(path).date_format == DateFormat::MonthDayYear); assert(load_config(path).wrist.width == .30f); auto example = load_config(std::filesystem::path(argv[1]) / "config.example.json"); assert(example.buttons.at("ptt") == "/user/hand/right/input/x"); @@ -37,6 +51,7 @@ int main(int argc, char** argv) { assert(!example.experimental_input_priority); assert(!example.advanced_debug); assert(!example.auto_insert); + 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_advanced_debug(path, true)); @@ -48,6 +63,12 @@ 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_clock_24h(path, true) && load_config(path).clock_24h); + assert(save_date_format(path, DateFormat::Iso) && load_config(path).date_format == DateFormat::Iso); + assert(save_date_format(path, DateFormat::Off) && load_config(path).date_format == DateFormat::Off); + assert(save_date_format(path, DateFormat::DayMonthYear) && load_config(path).date_format == DateFormat::DayMonthYear); + assert(save_date_format(path, DateFormat::MonthDayYear)); + assert(save_clock_24h(path, false)); for (const auto* invalid : {R"({"advanced_debug":"true"})", R"({"advanced_debug":0})", R"({"advanced_debug":null})", R"({"advanced_debug":[]})"}) { put(path, invalid); @@ -86,6 +107,16 @@ int main(int argc, char** argv) { assert(experimental.experimental_input_priority && experimental.advanced_debug); // A priority request must preserve the user's existing action manifest/bindings. assert(action_manifest(argv[1], experimental) == std::filesystem::absolute(std::filesystem::path(argv[1]) / "actions.json")); + for (const auto* invalid : {R"({"clock_24h":"true"})", R"({"date_format":true})", + R"({"date_format":"us"})"}) { + put(path, invalid); fails([&] { load_config(path); }); + assert(!save_clock_24h(path, true)); + assert(!save_date_format(path, DateFormat::Iso)); + assert(get(path) == invalid); + } + put(path, R"({"date_format":"off","font":"kept"})"); + assert(save_date_format(path, DateFormat::Iso)); + assert(get(path) == R"({"date_format":"iso","font":"kept"})"); put(path, R"({"auto_insert":"on"})"); fails([&] { load_config(path); }); assert(!save_auto_insert(path, true)); diff --git a/tests/panel_test.cpp b/tests/panel_test.cpp index 6fce769..d482e35 100644 --- a/tests/panel_test.cpp +++ b/tests/panel_test.cpp @@ -1,5 +1,6 @@ #include "panel_surface.hpp" #include +#include #include #include #include @@ -13,7 +14,7 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0) } void no_action(const SurfaceEvent& event) { assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug && !event.auto_insert && - !event.recenter && !event.open_bindings); + !event.clock_24h && !event.date_format && !event.recenter && !event.open_bindings); } void snapshot(PanelSurface& surface, const std::string& path) { std::ofstream out(path, std::ios::binary); @@ -31,6 +32,11 @@ int main(int argc, char** argv) { assert(surface.render(p)); assert(surface.pixels().size() == size_t(PanelSurface::width * PanelSurface::height * 4)); assert(!surface.render(p)); + surface.set_clock_time(std::time_t{1704211440}); // local clock changes only when rendered minute changes + assert(surface.render(p)); + assert(!surface.render(p)); + surface.set_clock_time(std::time_t{1704211500}); + assert(surface.render(p)); surface.reset_pointers(); assert(!surface.render(p)); // repeated inactive-input resets do not force uploads assert(surface.pixels()[3] == 0); // rounded outer corner is transparent @@ -175,6 +181,18 @@ int main(int argc, char** argv) { debug = click(surface, 680, 474); assert(debug.advanced_debug == false); surface.set_advanced_debug(false); assert(surface.render(p)); + auto clock = click(surface, 200, 528); + assert(clock.clock_24h == true && !clock.action); + assert(!surface.render(p)); // setting request waits for caller's accepted value + surface.set_clock_24h(true); assert(surface.render(p)); + assert(click(surface, 200, 528).clock_24h == false); + surface.set_clock_24h(false); assert(surface.render(p)); + for (DateFormat format : {DateFormat::DayMonthYear, DateFormat::Iso, DateFormat::Off, DateFormat::MonthDayYear}) { + auto date = click(surface, 680, 528); + assert(date.date_format == format && !date.action); + surface.set_date_format(format); + assert(surface.render(p)); + } assert(click(surface, 280, 610).action == UiAction::Cancel); surface.set_placement_note("Wrist not tracked - using world space until it returns."); assert(surface.render(p)); assert(!surface.render(p)); @@ -188,6 +206,8 @@ int main(int argc, char** argv) { no_action(click(surface, 680, 420)); // laser toggle only exists on Settings no_action(click(surface, 680, 474)); // debug toggle only exists on Settings no_action(click(surface, 200, 474)); // auto insert only exists on Settings + no_action(click(surface, 200, 528)); // clock/date controls only exist on Settings + no_action(click(surface, 680, 528)); // Bindings opens SteamVR directly, from either tab, without replacing review. surface.pointer_down(1, 480, 610); diff --git a/tests/test_installer.py b/tests/test_installer.py index ff617cb..1a01632 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -129,6 +129,8 @@ class InstallTests(unittest.TestCase): self.assertEqual(fixed["input_priority"], "normal") self.assertIs(fixed["advanced_debug"], False) self.assertIs(fixed["auto_insert"], False) + self.assertIs(fixed["clock_24h"], False) + self.assertEqual(fixed["date_format"], "mdy") self.assertEqual(fixed["wrist"], installer.CONFIG_DEFAULTS["wrist"]) backups = list(config.parent.glob("config.json.backup-*")) self.assertEqual(len(backups), 1) @@ -136,6 +138,8 @@ class InstallTests(unittest.TestCase): fixed["input_priority"] = "experimental" fixed["advanced_debug"] = True fixed["auto_insert"] = True + fixed["clock_24h"] = True + fixed["date_format"] = "iso" fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades fixed["wrist"]["y"] = 0.2 compact = json.dumps(fixed, separators=(",", ":")).encode() @@ -144,6 +148,8 @@ class InstallTests(unittest.TestCase): self.assertEqual(config.read_bytes(), compact) self.assertIs(json.loads(config.read_text())["advanced_debug"], True) self.assertIs(json.loads(config.read_text())["auto_insert"], True) + self.assertIs(json.loads(config.read_text())["clock_24h"], True) + self.assertEqual(json.loads(config.read_text())["date_format"], "iso") self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1) fixed["advanced_debug"] = False compact_off = json.dumps(fixed, separators=(",", ":")).encode()