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

+31 -5
View File
@@ -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<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 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<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.
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.
+19 -1
View File
@@ -10,6 +10,7 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <ctime>
#include <cmath>
#include <cstdlib>
#include <filesystem>
@@ -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);
+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_clock.hpp"
#include <ft2build.h>
#include FT_FREETYPE_H
#include <algorithm>
@@ -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<Button, 18> buttons{{
constexpr std::array<Button, 20> 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<Button, 18> 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<UiAction> action(Control c) {
switch (c) {
@@ -70,15 +74,6 @@ std::optional<Mount> 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<unsigned char>(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<int, 2> 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<DateFormat>((static_cast<int>(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
+6
View File
@@ -2,6 +2,7 @@
#include "overlay.hpp"
#include "config.hpp"
#include "panel_drag.hpp"
#include <ctime>
#include <memory>
#include <optional>
#include <string>
@@ -14,6 +15,8 @@ struct SurfaceEvent {
std::optional<bool> lasers_anytime;
std::optional<bool> advanced_debug;
std::optional<bool> auto_insert;
std::optional<bool> clock_24h;
std::optional<DateFormat> 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: