Show local clock and configurable date in panel header

This commit is contained in:
baketnk committed 2026-09-24 16:44:32 -04:00
1 parent 50f5277f5c
commit 10a11510ba
14 files changed
+218 -28

No files matched your search

+2 -1
View File
@@ -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. **Left grip (when active):** double-tap for the same explicit Enter action.
Nothing submits automatically. Auto Insert is opt-in and off by default. Nothing submits automatically. Auto Insert is opt-in and off by default.
- **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit. - **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 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 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 transparent margins, like Steam's window handles. Scaling keeps the upper-left
+2
View File
@@ -3,6 +3,8 @@
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": false, "advanced_debug": false,
"auto_insert": 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}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
"theme": { "theme": {
"background": "#0c101b", "background": "#0c101b",
+14
View File
@@ -51,6 +51,12 @@ requires an on-device comparison. The
[Vulkan deployment record](evidence/vulkan-overlay-2026-09-24.md) documents the [Vulkan deployment record](evidence/vulkan-overlay-2026-09-24.md) documents the
native installation and offscreen GPU checks separately from headset acceptance. 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 The complete transcript preview is paginated by glyph width and four-line
height; Previous and Next navigate it without changing the source transcript. height; Previous and Next navigate it without changing the source transcript.
Long status/detail messages show a prefix with a visible truncation marker. 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", "input_priority": "normal",
"advanced_debug": false, "advanced_debug": false,
"auto_insert": 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}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
"theme": { "theme": {
"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9", "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 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. 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 `advanced_debug` is a boolean (default `false`, not a string): an opt-in
+6
View File
@@ -36,6 +36,8 @@ CONFIG_DEFAULTS = {
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": False, "advanced_debug": False,
"auto_insert": 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}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
"theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9", "theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87", "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 fixed["advanced_debug"] = debug if type(debug) is bool else False
automatic = data.get("auto_insert", False) automatic = data.get("auto_insert", False)
fixed["auto_insert"] = automatic if type(automatic) is bool else 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 = data.get("wrist")
source = source if isinstance(source, dict) else {} source = source if isinstance(source, dict) else {}
fixed["wrist"] = {} fixed["wrist"] = {}
+6
View File
@@ -25,6 +25,8 @@ CONFIG_DEFAULTS = {
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": False, "advanced_debug": False,
"auto_insert": 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}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
"theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9", "theme": {"background": "#0c101b", "card": "#141c2b", "ink": "#e6f0f9",
"muted": "#97adc1", "accent": "#1ff0a4", "warning": "#ff6e87", "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 fixed["advanced_debug"] = debug if type(debug) is bool else False
automatic = data.get("auto_insert", False) automatic = data.get("auto_insert", False)
fixed["auto_insert"] = automatic if type(automatic) is bool else 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 = data.get("wrist")
source = source if isinstance(source, dict) else {} source = source if isinstance(source, dict) else {}
fixed["wrist"] = {} fixed["wrist"] = {}
+31 -5
View File
@@ -200,6 +200,16 @@ Config load_config(const std::filesystem::path& path) {
} else if (key == "auto_insert") { } else if (key == "auto_insert") {
if (!value.is_bool) throw std::runtime_error("Config auto_insert must be a boolean"); if (!value.is_bool) throw std::runtime_error("Config auto_insert must be a boolean");
config.auto_insert = value.value == "true"; 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") { } else if (key == "input_priority") {
if (!value.is_string || (value.value != "normal" && value.value != "experimental")) if (!value.is_string || (value.value != "normal" && value.value != "experimental"))
throw std::runtime_error("Config input_priority must be normal or 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; return config;
} }
namespace { 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 { try {
if (path.empty() || !path.is_absolute() || std::filesystem::is_symlink(path)) return false; if (path.empty() || !path.is_absolute() || std::filesystem::is_symlink(path)) return false;
const bool existing = std::filesystem::exists(path); 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}; Parser parser{bytes};
auto root = parser.parse(); parser.ws(); auto root = parser.parse(); parser.ws();
if (!root.is_object || parser.pos != bytes.size()) return false; 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)); auto it = root.object.find(std::string(key));
if (it != root.object.end()) { 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; 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 { } else {
bytes.insert(root.end - 1, std::string(root.object.empty() ? "" : ",") + 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. // The native reader rejects files >=4097 bytes, even if the JSON is valid.
if (bytes.size() > 4096) return false; 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; 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 } // namespace
bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept { bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept {
return save_bool_option(path, "advanced_debug", enabled); 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 { bool save_auto_insert(const std::filesystem::path& path, bool enabled) noexcept {
return save_bool_option(path, "auto_insert", enabled); 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) { std::string resolve_font(const std::string& assets, const std::string& requested) {
const auto bundled = std::filesystem::path(assets) / "fonts/Inconsolata-Regular.ttf"; const auto bundled = std::filesystem::path(assets) / "fonts/Inconsolata-Regular.ttf";
const std::array<std::filesystem::path, 4> candidates{requested, bundled, const std::array<std::filesystem::path, 4> candidates{requested, bundled,
+5
View File
@@ -13,6 +13,7 @@ struct Theme {
Rgba accent{31, 240, 164, 255}, warning{255, 110, 135, 255}; Rgba accent{31, 240, 164, 255}, warning{255, 110, 135, 255};
Rgba frame_start{31, 255, 145, 255}, frame_end{31, 112, 255, 255}; Rgba frame_start{31, 255, 145, 255}, frame_end{31, 112, 255, 255};
}; };
enum class DateFormat { Off, MonthDayYear, DayMonthYear, Iso };
struct Config { struct Config {
Theme theme; Theme theme;
std::string font; // absolute TTF/OTF path; empty uses the bundled face std::string font; // absolute TTF/OTF path; empty uses the bundled face
@@ -20,6 +21,8 @@ struct Config {
bool experimental_input_priority = false; bool experimental_input_priority = false;
bool advanced_debug = false; // opt-in full diagnostic logging; never raw audio recording 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 auto_insert = false; // opt-in; runtime also requires uninterrupted verified Xwayland focus
bool clock_24h = false;
DateFormat date_format = DateFormat::MonthDayYear;
WristPlacement wrist; WristPlacement wrist;
// OpenVR action name -> physical Frame controller input path; empty disables it. // OpenVR action name -> physical Frame controller input path; empty disables it.
std::map<std::string, std::string> buttons; std::map<std::string, std::string> 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. // 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_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept;
bool save_auto_insert(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); std::string resolve_font(const std::string& assets, const std::string& requested);
// No writes or OpenVR access when no custom button mappings are specified. // No writes or OpenVR access when no custom button mappings are specified.
// When customized, build a generated manifest and bindings under XDG cache. // When customized, build a generated manifest and bindings under XDG cache.
+19 -1
View File
@@ -10,6 +10,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <chrono> #include <chrono>
#include <ctime>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
@@ -179,6 +180,8 @@ struct Overlay::Impl {
surface.set_lasers_anytime(lasers_anytime); surface.set_lasers_anytime(lasers_anytime);
surface.set_advanced_debug(config.advanced_debug); surface.set_advanced_debug(config.advanced_debug);
surface.set_auto_insert(config.auto_insert); 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"); overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard");
vr::HmdVector2_t mouse_scale{{float(W), float(H)}}; vr::HmdVector2_t mouse_scale{{float(W), float(H)}};
overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale"); 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); } bool available(UiAction action) const { return surface.available(action); }
void draw(const Panel& p) { void draw(const Panel& p) {
panel = p; panel = p;
surface.set_clock_time(std::time(nullptr));
if (surface.render(p)) { if (surface.render(p)) {
gpu_texture->upload(surface.pixels()); gpu_texture->upload(surface.pixels());
auto texture = gpu_texture->texture(); auto texture = gpu_texture->texture();
@@ -473,8 +477,22 @@ struct Overlay::Impl {
last_pointer_event = "up button=" + std::to_string(event.data.mouse.button); last_pointer_event = "up button=" + std::to_string(event.data.mouse.button);
if (event.data.mouse.button == vr::VRMouseButton_Left) { 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); 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.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) { if (event_result.auto_insert) {
config.auto_insert = *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); auto_save_failed = persist_mount && !save_auto_insert(default_config_path(), config.auto_insert);
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "config.hpp"
#include <array>
#include <ctime>
#include <string>
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<char, 48> 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
+42 -20
View File
@@ -1,4 +1,5 @@
#include "panel_surface.hpp" #include "panel_surface.hpp"
#include "panel_clock.hpp"
#include <ft2build.h> #include <ft2build.h>
#include FT_FREETYPE_H #include FT_FREETYPE_H
#include <algorithm> #include <algorithm>
@@ -28,10 +29,11 @@ struct Rect {
} }
}; };
enum class Control { Review, Settings, Bindings, Prev, Next, Record, Cancel, Insert, Enter, Quit, 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 }; enum class Tab { Review, Settings };
struct Button { Rect r; Control id; const char* label; }; struct Button { Rect r; Control id; const char* label; };
constexpr std::array<Button, 18> buttons{{ constexpr std::array<Button, 20> buttons{{
{{32, 138, 180, 46}, Control::Review, "Review"}, {{32, 138, 180, 46}, Control::Review, "Review"},
{{226, 138, 180, 46}, Control::Settings, "Settings"}, {{226, 138, 180, 46}, Control::Settings, "Settings"},
{{420, 138, 180, 46}, Control::Bindings, "Bindings"}, {{420, 138, 180, 46}, Control::Bindings, "Bindings"},
@@ -50,6 +52,8 @@ constexpr std::array<Button, 18> buttons{{
{{514, 394, 454, 50}, Control::LasersAnytime, "Lasers anytime"}, {{514, 394, 454, 50}, Control::LasersAnytime, "Lasers anytime"},
{{32, 452, 454, 48}, Control::AutoInsert, "Auto insert"}, {{32, 452, 454, 48}, Control::AutoInsert, "Auto insert"},
{{514, 452, 454, 48}, Control::AdvancedDebug, "Advanced debug"}, {{514, 452, 454, 48}, Control::AdvancedDebug, "Advanced debug"},
{{32, 506, 454, 42}, Control::Clock24h, "Clock"},
{{514, 506, 454, 42}, Control::Date, "Date"},
}}; }};
std::optional<UiAction> action(Control c) { std::optional<UiAction> action(Control c) {
switch (c) { switch (c) {
@@ -70,15 +74,6 @@ std::optional<Mount> mounting(Control c) {
default: return {}; 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. // Invalid bytes become visible replacement glyphs, never control commands.
uint32_t next_codepoint(std::string_view s, size_t& i) { uint32_t next_codepoint(std::string_view s, size_t& i) {
const auto a = static_cast<unsigned char>(s[i++]); const auto a = static_cast<unsigned char>(s[i++]);
@@ -109,6 +104,10 @@ struct PanelSurface::Impl {
Color background, card, ink, muted, cyan, pink; Color background, card, ink, muted, cyan, pink;
Tab tab = Tab::Review; Tab tab = Tab::Review;
bool dirty = true, lasers_anytime = false, advanced_debug = false, auto_insert = false; 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::string placement_note, binding_note;
std::array<int, 2> pressed{{-1, -1}}; std::array<int, 2> pressed{{-1, -1}};
int drag_cursor = -1; int drag_cursor = -1;
@@ -254,7 +253,8 @@ struct PanelSurface::Impl {
bool visible(Control c) const { bool visible(Control c) const {
if (c == Control::Prev || c == Control::Next) return tab == Tab::Review; if (c == Control::Prev || c == Control::Next) return tab == Tab::Review;
if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime || 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; return true;
} }
bool enabled(Control c) const { 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) if (p.status != panel.status || p.detail != panel.detail || p.enabled != panel.enabled || p.recording != panel.recording)
dirty = true; dirty = true;
panel = p; 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; if (!dirty) return false;
displayed_clock = std::move(label);
std::fill(pixels.begin(), pixels.end(), 0); std::fill(pixels.begin(), pixels.end(), 0);
rect({0, 0, CW, CH}, background); rect({0, 0, CW, CH}, background);
frame(); frame();
text("FrameYap", 32, 61, 40, ink, 300); 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(displayed_clock.time, 475, 58, 28, ink, 735);
text(mount_label(mount), 756, 58, 24, cyan, 968); if (!displayed_clock.date.empty()) text(displayed_clock.date, 756, 58, 24, cyan, 968);
rounded({32, 78, 936, 48}, 13, mix(background, card, .7f), rounded({32, 78, 936, 48}, 13, mix(background, card, .7f),
panel.recording ? mix(card, pink, .36f) : mix(card, cyan, .22f), panel.recording ? mix(card, pink, .36f) : mix(card, cyan, .22f),
panel.recording ? .16f : 0.f); 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 (detail.size() > 2) text("[detail truncated]", 730, 546, 22, pink, 968);
if (!binding_note.empty()) text(binding_note, 32, 203, 20, muted, 968); if (!binding_note.empty()) text(binding_note, 32, 203, 20, muted, 968);
} else { } else {
text("MOUNT AND INTERACTION", 32, 224, 22, muted, 968); text("Auto insert needs stable X focus; debug logs may contain speech.", 32, 204, 18, pink, 968);
text("Auto insert requires stable X focus; never Enter. Debug logs may contain speech.", 32, 520, 20, pink, 968); text(placement_note.empty() ? "No automatic Enter; changing debug restarts the worker." : placement_note,
text(placement_note.empty() ? "Debug restarts worker; lasers may affect games. No saved audio clips." : placement_note, 32, 225, 18, muted, 968);
32, 540, 20, muted, 968);
} }
rect({32, 550, 936, 1}, mix(card, cyan, .17f)); rect({32, 550, 936, 1}, mix(card, cyan, .17f));
for (size_t i = 0; i < buttons.size(); ++i) { 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, rounded(b.r, std::min(16, b.r.h / 3), fill,
!on ? mix(card, muted, .13f) : highlighted ? accent : mix(card, muted, .38f), !on ? mix(card, muted, .13f) : highlighted ? accent : mix(card, muted, .38f),
highlighted ? .23f : 0.f, highlighted ? 2 : 1); highlighted ? .23f : 0.f, highlighted ? 2 : 1);
const auto label = b.id == Control::Record && panel.recording ? "Stop" : b.label; const std::string label = b.id == Control::Record && panel.recording ? "Stop" :
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); 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 (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) { if (b.id == Control::LasersAnytime || b.id == Control::AdvancedDebug || b.id == Control::AutoInsert) {
bool active = b.id == Control::LasersAnytime ? lasers_anytime : 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::LasersAnytime) result.lasers_anytime = !impl_->lasers_anytime;
else if (c == Control::AdvancedDebug) result.advanced_debug = !impl_->advanced_debug; 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::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<DateFormat>((static_cast<int>(impl_->date_format) + 1) % 4);
else if (c == Control::Bindings) { result.open_bindings = true; impl_->reset(); } else if (c == Control::Bindings) { result.open_bindings = true; impl_->reset(); }
else if (c == Control::Review || c == Control::Settings) { else if (c == Control::Review || c == Control::Settings) {
impl_->tab = c == Control::Review ? Tab::Review : Tab::Settings; impl_->tab = c == Control::Review ? Tab::Review : Tab::Settings;
@@ -442,4 +451,17 @@ void PanelSurface::set_auto_insert(bool enabled) {
impl_->dirty = true; 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 } // namespace frameyap
+6
View File
@@ -2,6 +2,7 @@
#include "overlay.hpp" #include "overlay.hpp"
#include "config.hpp" #include "config.hpp"
#include "panel_drag.hpp" #include "panel_drag.hpp"
#include <ctime>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -14,6 +15,8 @@ struct SurfaceEvent {
std::optional<bool> lasers_anytime; std::optional<bool> lasers_anytime;
std::optional<bool> advanced_debug; std::optional<bool> advanced_debug;
std::optional<bool> auto_insert; std::optional<bool> auto_insert;
std::optional<bool> clock_24h;
std::optional<DateFormat> date_format;
bool recenter = false; bool recenter = false;
bool open_bindings = false; bool open_bindings = false;
}; };
@@ -41,6 +44,9 @@ public:
void set_lasers_anytime(bool enabled); void set_lasers_anytime(bool enabled);
void set_advanced_debug(bool enabled); void set_advanced_debug(bool enabled);
void set_auto_insert(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); void set_binding_note(std::string note);
bool available(UiAction action) const; bool available(UiAction action) const;
private: private:
+31
View File
@@ -1,4 +1,5 @@
#include "config.hpp" #include "config.hpp"
#include "panel_clock.hpp"
#include <cassert> #include <cassert>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
@@ -14,6 +15,17 @@ template<class Fn> void fails(Fn fn) { bool raised = false; try { fn(); } catch
} }
int main(int argc, char** argv) { int main(int argc, char** argv) {
assert(argc == 2); 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())); const auto dir = std::filesystem::temp_directory_path() / ("frameyap-config-test-" + std::to_string(::getpid()));
std::filesystem::create_directories(dir); std::filesystem::create_directories(dir);
::setenv("XDG_CONFIG_HOME", dir.c_str(), 1); ::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).experimental_input_priority);
assert(!load_config(path).advanced_debug); assert(!load_config(path).advanced_debug);
assert(!load_config(path).auto_insert); 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); assert(load_config(path).wrist.width == .30f);
auto example = load_config(std::filesystem::path(argv[1]) / "config.example.json"); auto example = load_config(std::filesystem::path(argv[1]) / "config.example.json");
assert(example.buttons.at("ptt") == "/user/hand/right/input/x"); 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.experimental_input_priority);
assert(!example.advanced_debug); assert(!example.advanced_debug);
assert(!example.auto_insert); assert(!example.auto_insert);
assert(!example.clock_24h && example.date_format == DateFormat::MonthDayYear);
assert(example.wrist.y == .18f && example.wrist.z == .089f); assert(example.wrist.y == .18f && example.wrist.z == .089f);
std::filesystem::create_directories(path.parent_path()); std::filesystem::create_directories(path.parent_path());
assert(save_advanced_debug(path, true)); assert(save_advanced_debug(path, true));
@@ -48,6 +63,12 @@ int main(int argc, char** argv) {
assert(load_config(path).auto_insert); assert(load_config(path).auto_insert);
assert(save_auto_insert(path, false)); assert(save_auto_insert(path, false));
assert(!load_config(path).auto_insert); 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})", for (const auto* invalid : {R"({"advanced_debug":"true"})", R"({"advanced_debug":0})",
R"({"advanced_debug":null})", R"({"advanced_debug":[]})"}) { R"({"advanced_debug":null})", R"({"advanced_debug":[]})"}) {
put(path, invalid); put(path, invalid);
@@ -86,6 +107,16 @@ int main(int argc, char** argv) {
assert(experimental.experimental_input_priority && experimental.advanced_debug); assert(experimental.experimental_input_priority && experimental.advanced_debug);
// A priority request must preserve the user's existing action manifest/bindings. // 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")); 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"})"); put(path, R"({"auto_insert":"on"})");
fails([&] { load_config(path); }); fails([&] { load_config(path); });
assert(!save_auto_insert(path, true)); assert(!save_auto_insert(path, true));
+21 -1
View File
@@ -1,5 +1,6 @@
#include "panel_surface.hpp" #include "panel_surface.hpp"
#include <cassert> #include <cassert>
#include <ctime>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <limits> #include <limits>
@@ -13,7 +14,7 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0)
} }
void no_action(const SurfaceEvent& event) { void no_action(const SurfaceEvent& event) {
assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug && !event.auto_insert && 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) { void snapshot(PanelSurface& surface, const std::string& path) {
std::ofstream out(path, std::ios::binary); std::ofstream out(path, std::ios::binary);
@@ -31,6 +32,11 @@ int main(int argc, char** argv) {
assert(surface.render(p)); assert(surface.render(p));
assert(surface.pixels().size() == size_t(PanelSurface::width * PanelSurface::height * 4)); assert(surface.pixels().size() == size_t(PanelSurface::width * PanelSurface::height * 4));
assert(!surface.render(p)); 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(); surface.reset_pointers();
assert(!surface.render(p)); // repeated inactive-input resets do not force uploads assert(!surface.render(p)); // repeated inactive-input resets do not force uploads
assert(surface.pixels()[3] == 0); // rounded outer corner is transparent 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); debug = click(surface, 680, 474);
assert(debug.advanced_debug == false); assert(debug.advanced_debug == false);
surface.set_advanced_debug(false); assert(surface.render(p)); 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); assert(click(surface, 280, 610).action == UiAction::Cancel);
surface.set_placement_note("Wrist not tracked - using world space until it returns."); surface.set_placement_note("Wrist not tracked - using world space until it returns.");
assert(surface.render(p)); assert(!surface.render(p)); 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, 420)); // laser toggle only exists on Settings
no_action(click(surface, 680, 474)); // debug 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, 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. // Bindings opens SteamVR directly, from either tab, without replacing review.
surface.pointer_down(1, 480, 610); surface.pointer_down(1, 480, 610);
+6
View File
@@ -129,6 +129,8 @@ class InstallTests(unittest.TestCase):
self.assertEqual(fixed["input_priority"], "normal") self.assertEqual(fixed["input_priority"], "normal")
self.assertIs(fixed["advanced_debug"], False) self.assertIs(fixed["advanced_debug"], False)
self.assertIs(fixed["auto_insert"], 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"]) self.assertEqual(fixed["wrist"], installer.CONFIG_DEFAULTS["wrist"])
backups = list(config.parent.glob("config.json.backup-*")) backups = list(config.parent.glob("config.json.backup-*"))
self.assertEqual(len(backups), 1) self.assertEqual(len(backups), 1)
@@ -136,6 +138,8 @@ class InstallTests(unittest.TestCase):
fixed["input_priority"] = "experimental" fixed["input_priority"] = "experimental"
fixed["advanced_debug"] = True fixed["advanced_debug"] = True
fixed["auto_insert"] = True fixed["auto_insert"] = True
fixed["clock_24h"] = True
fixed["date_format"] = "iso"
fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades
fixed["wrist"]["y"] = 0.2 fixed["wrist"]["y"] = 0.2
compact = json.dumps(fixed, separators=(",", ":")).encode() compact = json.dumps(fixed, separators=(",", ":")).encode()
@@ -144,6 +148,8 @@ class InstallTests(unittest.TestCase):
self.assertEqual(config.read_bytes(), compact) self.assertEqual(config.read_bytes(), compact)
self.assertIs(json.loads(config.read_text())["advanced_debug"], True) 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())["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) self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1)
fixed["advanced_debug"] = False fixed["advanced_debug"] = False
compact_off = json.dumps(fixed, separators=(",", ":")).encode() compact_off = json.dumps(fixed, separators=(",", ":")).encode()