feat: add NTP; fix networking bugs/regressions
This commit is contained in:
@@ -220,7 +220,7 @@ charmap: libc
|
||||
printers: bearssl libc tls
|
||||
$(MAKE) -C src/printers
|
||||
|
||||
# Build time zone standalone GUI tool (depends on libc).
|
||||
# Build Time standalone GUI tool (depends on libc).
|
||||
timezone: libc
|
||||
$(MAKE) -C src/timezone
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[ntp]
|
||||
enabled = true
|
||||
server = "pool.ntp.org"
|
||||
@@ -211,6 +211,7 @@ namespace montauk::abi {
|
||||
|
||||
// Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported.
|
||||
static constexpr uint64_t SYS_STAT = 152;
|
||||
static constexpr uint64_t SYS_SETUNIXTIME = 153;
|
||||
|
||||
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
||||
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
||||
|
||||
@@ -176,6 +176,7 @@ extern "C" {
|
||||
#define MTK_SYS_FBFLIP 150
|
||||
#define MTK_SYS_GETEXECPATH 151
|
||||
#define MTK_SYS_STAT 152
|
||||
#define MTK_SYS_SETUNIXTIME 153
|
||||
/* @SYSCALLS-END */
|
||||
|
||||
#define MTK_SOCK_TCP 1
|
||||
@@ -596,6 +597,10 @@ static inline void mtk_gettime(mtk_datetime *out) {
|
||||
_mtk_syscall1(MTK_SYS_GETTIME, (long)out);
|
||||
}
|
||||
|
||||
static inline int mtk_set_unix_time(int64_t unix_seconds) {
|
||||
return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds);
|
||||
}
|
||||
|
||||
static inline void mtk_settz(int offset_minutes) {
|
||||
_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* ntp.h
|
||||
* Small NTPv4 client for MontaukOS programs (RFC 5905).
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
namespace montauk::ntp {
|
||||
|
||||
static constexpr uint16_t SERVER_PORT = 123;
|
||||
static constexpr uint32_t PACKET_SIZE = 48;
|
||||
static constexpr uint64_t UNIX_EPOCH_DELTA = 2208988800ULL;
|
||||
static constexpr uint32_t MIN_QUERY_INTERVAL_MS = 64000;
|
||||
|
||||
enum Result {
|
||||
OK = 0,
|
||||
INVALID_SERVER = -1,
|
||||
SOCKET_ERROR = -2,
|
||||
SEND_ERROR = -3,
|
||||
TIMEOUT = -4,
|
||||
INVALID_REPLY = -5,
|
||||
CLOCK_ERROR = -6,
|
||||
SERVER_REFUSED = -7,
|
||||
};
|
||||
|
||||
using ProgressFn = void (*)(const char* message);
|
||||
|
||||
inline void progress(ProgressFn callback, const char* message) {
|
||||
if (callback) callback(message);
|
||||
}
|
||||
|
||||
inline const char* result_string(int result) {
|
||||
switch (result) {
|
||||
case OK: return "Time synchronized";
|
||||
case INVALID_SERVER: return "Could not resolve NTP server";
|
||||
case SOCKET_ERROR: return "Could not open NTP socket";
|
||||
case SEND_ERROR: return "Could not send NTP request";
|
||||
case TIMEOUT: return "NTP server timed out";
|
||||
case INVALID_REPLY: return "NTP server returned an invalid reply";
|
||||
case CLOCK_ERROR: return "Could not set the system clock";
|
||||
case SERVER_REFUSED: return "NTP server refused frequent requests";
|
||||
default: return "NTP synchronization failed";
|
||||
}
|
||||
}
|
||||
|
||||
inline uint32_t read_be32(const uint8_t* p) {
|
||||
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
||||
((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
inline void write_be64(uint8_t* p, uint64_t value) {
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
p[i] = (uint8_t)value;
|
||||
value >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool is_leap_year(int year) {
|
||||
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
||||
}
|
||||
|
||||
inline int days_in_month(int month, int year) {
|
||||
static const int days[] = {0, 31, 28, 31, 30, 31, 30,
|
||||
31, 31, 30, 31, 30, 31};
|
||||
return month == 2 && is_leap_year(year) ? 29 : days[month];
|
||||
}
|
||||
|
||||
inline int64_t current_unix_seconds() {
|
||||
montauk::abi::DateTime now = {};
|
||||
montauk::gettime(&now);
|
||||
if (now.Year < 1970 || now.Month < 1 || now.Month > 12 ||
|
||||
now.Day < 1 || now.Day > days_in_month(now.Month, now.Year))
|
||||
return 0;
|
||||
|
||||
int64_t days = 0;
|
||||
for (int year = 1970; year < (int)now.Year; year++)
|
||||
days += is_leap_year(year) ? 366 : 365;
|
||||
for (int month = 1; month < (int)now.Month; month++)
|
||||
days += days_in_month(month, now.Year);
|
||||
days += now.Day - 1;
|
||||
|
||||
int64_t local = days * 86400 + (int64_t)now.Hour * 3600 +
|
||||
(int64_t)now.Minute * 60 + now.Second;
|
||||
return local - (int64_t)montauk::gettz() * 60;
|
||||
}
|
||||
|
||||
inline bool same_bytes(const uint8_t* a, const uint8_t* b, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline int synchronize(const char* server, uint32_t timeout_ms = 5000,
|
||||
int64_t* out_unix_seconds = nullptr,
|
||||
ProgressFn progress_callback = nullptr) {
|
||||
if (!server || !server[0]) return INVALID_SERVER;
|
||||
|
||||
progress(progress_callback, "Opening NTP socket...");
|
||||
int fd = montauk::socket(montauk::abi::SOCK_UDP);
|
||||
if (fd < 0) return SOCKET_ERROR;
|
||||
|
||||
progress(progress_callback, "Resolving NTP server...");
|
||||
uint32_t server_ip = montauk::resolve(server);
|
||||
if (server_ip == 0) {
|
||||
montauk::closesocket(fd);
|
||||
return INVALID_SERVER;
|
||||
}
|
||||
|
||||
uint8_t request[PACKET_SIZE] = {};
|
||||
request[0] = 0x23; // leap=0, version=4, mode=3 (client)
|
||||
request[2] = 6; // poll exponent: 2^6 = 64 seconds
|
||||
request[3] = 0xEC; // precision: approximately 2^-20 seconds
|
||||
|
||||
int64_t unix_now = current_unix_seconds();
|
||||
if (unix_now <= 0) {
|
||||
montauk::closesocket(fd);
|
||||
return CLOCK_ERROR;
|
||||
}
|
||||
uint64_t transmit_timestamp =
|
||||
((uint64_t)unix_now + UNIX_EPOCH_DELTA) << 32;
|
||||
write_be64(request + 40, transmit_timestamp);
|
||||
|
||||
progress(progress_callback, "Sending NTP request...");
|
||||
if (montauk::sendto(fd, request, sizeof(request), server_ip, SERVER_PORT) < 0) {
|
||||
montauk::closesocket(fd);
|
||||
return SEND_ERROR;
|
||||
}
|
||||
|
||||
progress(progress_callback, "Waiting for NTP reply...");
|
||||
uint64_t deadline = montauk::get_milliseconds() + timeout_ms;
|
||||
uint8_t reply[PACKET_SIZE];
|
||||
for (;;) {
|
||||
uint32_t source_ip = 0;
|
||||
uint16_t source_port = 0;
|
||||
int received = montauk::recvfrom(fd, reply, sizeof(reply), &source_ip, &source_port);
|
||||
if (received >= (int)PACKET_SIZE && source_port == SERVER_PORT) {
|
||||
progress(progress_callback, "Validating NTP reply...");
|
||||
uint8_t leap = reply[0] >> 6;
|
||||
uint8_t version = (reply[0] >> 3) & 7;
|
||||
uint8_t mode = reply[0] & 7;
|
||||
uint8_t stratum = reply[1];
|
||||
if (leap == 3 || version < 3 || mode != 4 ||
|
||||
!same_bytes(reply + 24, request + 40, 8)) {
|
||||
montauk::closesocket(fd);
|
||||
return INVALID_REPLY;
|
||||
}
|
||||
// Stratum zero is a Kiss-o'-Death response, commonly returned
|
||||
// when a client sends requests too frequently.
|
||||
if (stratum == 0) {
|
||||
montauk::closesocket(fd);
|
||||
return SERVER_REFUSED;
|
||||
}
|
||||
if (stratum > 15) {
|
||||
montauk::closesocket(fd);
|
||||
return INVALID_REPLY;
|
||||
}
|
||||
|
||||
uint32_t ntp_seconds = read_be32(reply + 40);
|
||||
uint32_t fraction = read_be32(reply + 44);
|
||||
if ((uint64_t)ntp_seconds < UNIX_EPOCH_DELTA) {
|
||||
montauk::closesocket(fd);
|
||||
return INVALID_REPLY;
|
||||
}
|
||||
|
||||
int64_t unix_seconds =
|
||||
(int64_t)((uint64_t)ntp_seconds - UNIX_EPOCH_DELTA);
|
||||
if (fraction >= 0x80000000u) unix_seconds++;
|
||||
progress(progress_callback, "Applying synchronized time...");
|
||||
int applied = montauk::set_unix_time(unix_seconds);
|
||||
progress(progress_callback, "Closing NTP socket...");
|
||||
montauk::closesocket(fd);
|
||||
if (applied < 0) return CLOCK_ERROR;
|
||||
if (out_unix_seconds) *out_unix_seconds = unix_seconds;
|
||||
return OK;
|
||||
}
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now >= deadline) {
|
||||
montauk::closesocket(fd);
|
||||
return TIMEOUT;
|
||||
}
|
||||
// Poll with a short sleep instead of blocking on the socket handle.
|
||||
// This keeps the timeout owned by this loop and avoids a missed socket
|
||||
// wake leaving a caller blocked after its deadline.
|
||||
uint64_t remaining = deadline - now;
|
||||
montauk::sleep_ms(remaining < 10 ? remaining : 10);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace montauk::ntp
|
||||
@@ -367,6 +367,9 @@ namespace montauk {
|
||||
|
||||
// Timekeeping (wall-clock)
|
||||
inline void gettime(montauk::abi::DateTime* out) { syscall1(montauk::abi::SYS_GETTIME, (uint64_t)out); }
|
||||
inline int set_unix_time(int64_t unix_seconds) {
|
||||
return (int)syscall1(montauk::abi::SYS_SETUNIXTIME, (uint64_t)unix_seconds);
|
||||
}
|
||||
|
||||
// Timezone offset (total minutes from UTC)
|
||||
inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); }
|
||||
|
||||
@@ -163,6 +163,7 @@ extern "C" void _start() {
|
||||
|
||||
// ---- Stage 1: Network configuration (non-blocking) ----
|
||||
run_service("0:/os/dhcp.elf", "dhcp", false);
|
||||
run_service("0:/os/ntp.elf", "network time", false);
|
||||
|
||||
// Printing is an optional install component; skip quietly when absent.
|
||||
if (service_installed("0:/os/printd.elf"))
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Network Time Protocol service
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/ntp.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
static constexpr uint64_t RETRY_INTERVAL_MS =
|
||||
montauk::ntp::MIN_QUERY_INTERVAL_MS;
|
||||
static constexpr uint64_t SYNC_INTERVAL_MS = 60ULL * 60ULL * 1000ULL;
|
||||
static constexpr uint64_t CONFIG_POLL_MS = 5000;
|
||||
|
||||
static void log(const char* message) {
|
||||
montauk::print("ntp: ");
|
||||
montauk::print(message);
|
||||
montauk::print("\n");
|
||||
}
|
||||
|
||||
static bool load_settings(char* server, int server_cap) {
|
||||
auto cfg = montauk::config::load("ntp");
|
||||
bool enabled = cfg.get_bool("ntp.enabled", true);
|
||||
montauk::strncpy(server, cfg.get_string("ntp.server", "pool.ntp.org"),
|
||||
server_cap);
|
||||
cfg.destroy();
|
||||
if (!server[0])
|
||||
montauk::strncpy(server, "pool.ntp.org", server_cap);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
log("service started");
|
||||
|
||||
char active_server[128] = {};
|
||||
bool was_enabled = false;
|
||||
uint64_t next_attempt = 0;
|
||||
|
||||
for (;;) {
|
||||
char server[128];
|
||||
bool enabled = load_settings(server, sizeof(server));
|
||||
bool server_changed = !montauk::streq(server, active_server);
|
||||
if (server_changed) {
|
||||
montauk::strncpy(active_server, server, sizeof(active_server));
|
||||
next_attempt = 0;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
was_enabled = false;
|
||||
next_attempt = 0;
|
||||
montauk::sleep_ms(CONFIG_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
if (!was_enabled) {
|
||||
was_enabled = true;
|
||||
next_attempt = 0;
|
||||
}
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (next_attempt != 0 && now < next_attempt) {
|
||||
montauk::sleep_ms(CONFIG_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
montauk::abi::NetCfg net = {};
|
||||
montauk::get_netcfg(&net);
|
||||
if (net.ipAddress == 0 || net.dnsServer == 0) {
|
||||
next_attempt = now + RETRY_INTERVAL_MS;
|
||||
montauk::sleep_ms(CONFIG_POLL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
int result = montauk::ntp::synchronize(server);
|
||||
log(montauk::ntp::result_string(result));
|
||||
next_attempt = montauk::get_milliseconds() +
|
||||
(result == montauk::ntp::OK ? SYNC_INTERVAL_MS : RETRY_INTERVAL_MS);
|
||||
montauk::sleep_ms(CONFIG_POLL_MS);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Makefile for timezone (standalone Time Zone configuration app) on MontaukOS
|
||||
# Makefile for timezone (standalone Time configuration app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
|
||||
+271
-36
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Time Zone configuration app
|
||||
* MontaukOS Time configuration app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/ntp.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/mtk.hpp>
|
||||
@@ -23,13 +24,25 @@ using namespace gui;
|
||||
|
||||
static constexpr int INIT_W = 640;
|
||||
static constexpr int INIT_H = 460;
|
||||
static constexpr int HEADER_H = 14;
|
||||
static constexpr int TAB_H = 36;
|
||||
static constexpr int FOOTER_H = 58;
|
||||
static constexpr int PAD = 16;
|
||||
static constexpr int GAP = 12;
|
||||
static constexpr int LABEL_H = 24;
|
||||
static constexpr int ROW_H = 30;
|
||||
static constexpr int MAX_COUNTRIES = 320;
|
||||
static constexpr int NTP_SERVER_CAP = 128;
|
||||
|
||||
enum Tab {
|
||||
TAB_TIME_ZONES = 0,
|
||||
TAB_NTP = 1,
|
||||
TAB_COUNT = 2,
|
||||
};
|
||||
|
||||
static const char* const kTabLabels[TAB_COUNT] = {
|
||||
"Time Zones",
|
||||
"NTP",
|
||||
};
|
||||
|
||||
struct CountryEntry {
|
||||
char key[64]; // Full TOML table key, e.g. countries.NO
|
||||
@@ -43,6 +56,7 @@ struct CountryEntry {
|
||||
};
|
||||
|
||||
static WsWindow g_win;
|
||||
static Tab g_tab = TAB_TIME_ZONES;
|
||||
static montauk::toml::Doc g_data;
|
||||
static bool g_data_loaded = false;
|
||||
static CountryEntry g_countries[MAX_COUNTRIES];
|
||||
@@ -60,10 +74,20 @@ static int g_focus_pane = 0; // 0=country list, 1=zone list
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static bool g_dirty = false;
|
||||
static char g_ntp_server[NTP_SERVER_CAP] = "pool.ntp.org";
|
||||
static char g_saved_ntp_server[NTP_SERVER_CAP] = "pool.ntp.org";
|
||||
static mtk::TextInputState g_ntp_input = {};
|
||||
static bool g_ntp_enabled = true;
|
||||
static bool g_saved_ntp_enabled = true;
|
||||
static bool g_ntp_dirty = false;
|
||||
static bool g_ntp_syncing = false;
|
||||
static uint64_t g_last_clock_render = 0;
|
||||
static char g_status[160] = {};
|
||||
static uint64_t g_status_time = 0;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
|
||||
static void render();
|
||||
|
||||
static void safe_copy(char* dst, int cap, const char* src) {
|
||||
montauk::strncpy(dst, src ? src : "", cap);
|
||||
}
|
||||
@@ -217,8 +241,8 @@ static void clamp_scrolls() {
|
||||
Rect country_list = {};
|
||||
Rect zone_list = {};
|
||||
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
|
||||
int list_y = HEADER_H + LABEL_H;
|
||||
int list_h = gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H);
|
||||
int list_y = TAB_H + LABEL_H;
|
||||
int list_h = gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H);
|
||||
country_list = {PAD, list_y, left_w, list_h};
|
||||
zone_list = {PAD + left_w + GAP, list_y,
|
||||
g_win.width - PAD * 2 - GAP - left_w, list_h};
|
||||
@@ -235,8 +259,8 @@ static void clamp_scrolls() {
|
||||
}
|
||||
|
||||
static void ensure_country_visible() {
|
||||
Rect list = {PAD, HEADER_H + LABEL_H, 250,
|
||||
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
||||
Rect list = {PAD, TAB_H + LABEL_H, 250,
|
||||
gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
||||
int rows = visible_rows(list);
|
||||
if (g_selected_country < g_country_scroll)
|
||||
g_country_scroll = g_selected_country;
|
||||
@@ -246,9 +270,9 @@ static void ensure_country_visible() {
|
||||
|
||||
static void ensure_zone_visible() {
|
||||
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
|
||||
Rect list = {PAD + left_w + GAP, HEADER_H + LABEL_H,
|
||||
Rect list = {PAD + left_w + GAP, TAB_H + LABEL_H,
|
||||
g_win.width - PAD * 2 - GAP - left_w,
|
||||
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
||||
gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
||||
int rows = visible_rows(list);
|
||||
if (g_selected_zone < g_zone_scroll)
|
||||
g_zone_scroll = g_selected_zone;
|
||||
@@ -357,6 +381,73 @@ static void load_saved_selection() {
|
||||
clamp_scrolls();
|
||||
}
|
||||
|
||||
static void load_ntp_settings() {
|
||||
auto cfg = montauk::config::load("ntp");
|
||||
g_ntp_enabled = cfg.get_bool("ntp.enabled", true);
|
||||
safe_copy(g_ntp_server, sizeof(g_ntp_server),
|
||||
cfg.get_string("ntp.server", "pool.ntp.org"));
|
||||
cfg.destroy();
|
||||
if (!g_ntp_server[0])
|
||||
safe_copy(g_ntp_server, sizeof(g_ntp_server), "pool.ntp.org");
|
||||
safe_copy(g_saved_ntp_server, sizeof(g_saved_ntp_server), g_ntp_server);
|
||||
g_saved_ntp_enabled = g_ntp_enabled;
|
||||
g_ntp_dirty = false;
|
||||
mtk::text_input_reset(g_ntp_input, montauk::slen(g_ntp_server));
|
||||
}
|
||||
|
||||
static bool ntp_settings_changed() {
|
||||
return g_ntp_enabled != g_saved_ntp_enabled ||
|
||||
!montauk::streq(g_ntp_server, g_saved_ntp_server);
|
||||
}
|
||||
|
||||
static void save_ntp_settings() {
|
||||
if (!g_ntp_server[0]) {
|
||||
set_status("Enter an NTP server");
|
||||
return;
|
||||
}
|
||||
|
||||
auto cfg = montauk::config::load("ntp");
|
||||
montauk::config::set_bool(&cfg, "ntp.enabled", g_ntp_enabled);
|
||||
montauk::config::set_string(&cfg, "ntp.server", g_ntp_server);
|
||||
int result = montauk::config::save("ntp", &cfg);
|
||||
cfg.destroy();
|
||||
if (result < 0) {
|
||||
set_status("Could not save NTP settings");
|
||||
return;
|
||||
}
|
||||
|
||||
safe_copy(g_saved_ntp_server, sizeof(g_saved_ntp_server), g_ntp_server);
|
||||
g_saved_ntp_enabled = g_ntp_enabled;
|
||||
g_ntp_dirty = false;
|
||||
set_status("NTP settings saved");
|
||||
}
|
||||
|
||||
static void revert_ntp_settings() {
|
||||
safe_copy(g_ntp_server, sizeof(g_ntp_server), g_saved_ntp_server);
|
||||
g_ntp_enabled = g_saved_ntp_enabled;
|
||||
g_ntp_dirty = false;
|
||||
mtk::text_input_reset(g_ntp_input, montauk::slen(g_ntp_server));
|
||||
set_status("NTP changes reverted");
|
||||
}
|
||||
|
||||
static void ntp_progress(const char* message) {
|
||||
set_status(message);
|
||||
render();
|
||||
}
|
||||
|
||||
static void synchronize_ntp() {
|
||||
if (g_ntp_syncing) return;
|
||||
if (!g_ntp_server[0]) {
|
||||
set_status("Enter an NTP server");
|
||||
return;
|
||||
}
|
||||
g_ntp_syncing = true;
|
||||
int result = montauk::ntp::synchronize(g_ntp_server, 5000, nullptr,
|
||||
ntp_progress);
|
||||
g_ntp_syncing = false;
|
||||
set_status(montauk::ntp::result_string(result));
|
||||
}
|
||||
|
||||
static void fit_text(char* out, int cap, const char* text, int max_w) {
|
||||
safe_copy(out, cap, text);
|
||||
if (text_width(out) <= max_w) return;
|
||||
@@ -482,8 +573,8 @@ static void revert_selection() {
|
||||
|
||||
static Rect country_list_rect() {
|
||||
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
|
||||
return {PAD, HEADER_H + LABEL_H, left_w,
|
||||
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
||||
return {PAD, TAB_H + LABEL_H, left_w,
|
||||
gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
||||
}
|
||||
|
||||
static Rect zone_list_rect() {
|
||||
@@ -500,6 +591,30 @@ static Rect revert_button_rect() {
|
||||
return {g_win.width - PAD - 104 - GAP - 92, g_win.height - FOOTER_H + 14, 92, 30};
|
||||
}
|
||||
|
||||
static Rect tab_bar_rect() {
|
||||
return {0, 0, g_win.width, TAB_H};
|
||||
}
|
||||
|
||||
static int ntp_server_field_y() {
|
||||
return TAB_H + 24;
|
||||
}
|
||||
|
||||
static Rect ntp_server_input_rect() {
|
||||
return mtk::labeled_text_input_rect(PAD, ntp_server_field_y(),
|
||||
g_win.width - PAD * 2,
|
||||
app_theme(), 34);
|
||||
}
|
||||
|
||||
static Rect ntp_enabled_rect() {
|
||||
Rect input = ntp_server_input_rect();
|
||||
return {PAD, input.y + input.h + 18, g_win.width - PAD * 2, 28};
|
||||
}
|
||||
|
||||
static Rect ntp_sync_button_rect() {
|
||||
Rect revert = revert_button_rect();
|
||||
return {revert.x - GAP - 112, revert.y, 112, revert.h};
|
||||
}
|
||||
|
||||
static bool mouse_in_rect(const Rect& rect) {
|
||||
return rect.contains(g_mouse_x, g_mouse_y);
|
||||
}
|
||||
@@ -644,50 +759,92 @@ static void draw_details(Canvas& canvas, const mtk::Theme& theme) {
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_ntp_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
int field_y = ntp_server_field_y();
|
||||
mtk::draw_labeled_text_field(canvas, PAD, field_y, g_win.width - PAD * 2,
|
||||
"NTP Server", g_ntp_server,
|
||||
g_ntp_input.cursor, true, false, theme, 34,
|
||||
g_ntp_input.selection_anchor);
|
||||
|
||||
Rect enabled = ntp_enabled_rect();
|
||||
mtk::draw_checkbox(canvas, enabled, "Synchronize time automatically",
|
||||
mtk::check_state(g_ntp_enabled), theme, true,
|
||||
enabled.contains(g_mouse_x, g_mouse_y));
|
||||
|
||||
canvas.text(PAD, enabled.y + enabled.h + 10,
|
||||
"MontaukOS will synchronize at startup and periodically while enabled.",
|
||||
theme.text_subtle);
|
||||
|
||||
montauk::abi::DateTime now = {};
|
||||
montauk::gettime(&now);
|
||||
char current[96];
|
||||
snprintf(current, sizeof(current), "Current system time: %04u-%02u-%02u %02u:%02u:%02u",
|
||||
(unsigned)now.Year, (unsigned)now.Month, (unsigned)now.Day,
|
||||
(unsigned)now.Hour, (unsigned)now.Minute, (unsigned)now.Second);
|
||||
int explanation_y = enabled.y + enabled.h + 10;
|
||||
canvas.text(PAD, explanation_y + system_font_height() + 24,
|
||||
current, theme.text);
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas canvas = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
|
||||
canvas.fill(theme.window_bg);
|
||||
mtk::draw_tab_bar(canvas, tab_bar_rect(), kTabLabels, TAB_COUNT,
|
||||
(int)g_tab, theme);
|
||||
|
||||
if (g_country_count <= 0) {
|
||||
canvas.text(PAD, HEADER_H + 24,
|
||||
"No time zone data was found in 0:/config/timezonedata.toml",
|
||||
theme.danger);
|
||||
host.present();
|
||||
return;
|
||||
if (g_tab == TAB_TIME_ZONES) {
|
||||
if (g_country_count <= 0) {
|
||||
canvas.text(PAD, TAB_H + 24,
|
||||
"No time zone data was found in 0:/config/timezonedata.toml",
|
||||
theme.danger);
|
||||
} else {
|
||||
Rect country = country_list_rect();
|
||||
Rect zone = zone_list_rect();
|
||||
canvas.text(country.x, TAB_H + 6, "Country", theme.text_subtle);
|
||||
canvas.text(zone.x, TAB_H + 6, "Time Zone", theme.text_subtle);
|
||||
draw_country_list(canvas, country, theme);
|
||||
draw_zone_list(canvas, zone, theme);
|
||||
draw_details(canvas, theme);
|
||||
}
|
||||
} else {
|
||||
draw_ntp_tab(canvas, theme);
|
||||
}
|
||||
|
||||
Rect country = country_list_rect();
|
||||
Rect zone = zone_list_rect();
|
||||
|
||||
canvas.text(country.x, HEADER_H + 6, "Country", theme.text_subtle);
|
||||
canvas.text(zone.x, HEADER_H + 6, "Time Zone", theme.text_subtle);
|
||||
|
||||
draw_country_list(canvas, country, theme);
|
||||
draw_zone_list(canvas, zone, theme);
|
||||
draw_details(canvas, theme);
|
||||
|
||||
Rect footer = {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
|
||||
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
|
||||
mtk::draw_separator(canvas, 0, footer.y, g_win.width, theme);
|
||||
|
||||
const char* footer_text = status_visible()
|
||||
? g_status
|
||||
: (g_dirty ? "Unsaved time zone selection" : "Selection saved");
|
||||
int max_footer_text = revert_button_rect().x - PAD - GAP;
|
||||
bool dirty = g_tab == TAB_TIME_ZONES ? g_dirty : g_ntp_dirty;
|
||||
const char* footer_text = status_visible() ? g_status :
|
||||
(g_tab == TAB_TIME_ZONES
|
||||
? (g_dirty ? "Unsaved time zone selection" : "Selection saved")
|
||||
: (g_ntp_dirty ? "Unsaved NTP changes" : "NTP settings ready"));
|
||||
int max_footer_text = (g_tab == TAB_NTP ? ntp_sync_button_rect().x
|
||||
: revert_button_rect().x) - PAD - GAP;
|
||||
char footer_label[160];
|
||||
fit_text(footer_label, sizeof(footer_label), footer_text, max_footer_text);
|
||||
canvas.text(PAD, footer.y + (FOOTER_H - system_font_height()) / 2,
|
||||
footer_label, g_dirty ? theme.text : theme.text_subtle);
|
||||
footer_label, dirty ? theme.text : theme.text_subtle);
|
||||
|
||||
Rect revert = revert_button_rect();
|
||||
Rect apply = apply_button_rect();
|
||||
mtk::draw_button(canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
|
||||
button_state(revert, g_dirty), theme);
|
||||
button_state(revert, dirty), theme);
|
||||
mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
|
||||
button_state(apply, g_dirty), theme);
|
||||
button_state(apply, dirty), theme);
|
||||
if (g_tab == TAB_NTP) {
|
||||
Rect sync = ntp_sync_button_rect();
|
||||
mtk::draw_button(canvas, sync, g_ntp_syncing ? "Syncing..." : "Sync Now",
|
||||
mtk::BUTTON_PRIMARY,
|
||||
button_state(sync, !g_ntp_syncing), theme);
|
||||
mtk::draw_text_input_context_menu(
|
||||
canvas, g_ntp_input, theme,
|
||||
mtk::text_input_has_selection(g_ntp_input, g_ntp_server,
|
||||
(int)sizeof(g_ntp_server)));
|
||||
}
|
||||
|
||||
host.present();
|
||||
}
|
||||
@@ -698,6 +855,55 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
bool right_clicked = (ev.mouse.buttons & 2) && !(ev.mouse.prev_buttons & 2);
|
||||
if (clicked) {
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT,
|
||||
g_mouse_x, g_mouse_y);
|
||||
if (tab >= 0) {
|
||||
g_tab = (Tab)tab;
|
||||
mtk::context_menu_close(g_ntp_input.context);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_tab == TAB_NTP) {
|
||||
Rect input = ntp_server_input_rect();
|
||||
if (g_ntp_input.context.open || g_ntp_input.dragging ||
|
||||
input.contains(g_mouse_x, g_mouse_y)) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
g_ntp_input, input, g_ntp_server, (int)sizeof(g_ntp_server),
|
||||
g_mouse_x, g_mouse_y, ev.mouse.buttons, ev.mouse.prev_buttons,
|
||||
g_win.width, g_win.height, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_ntp_dirty = ntp_settings_changed();
|
||||
}
|
||||
if (result != mtk::TEXT_INPUT_NONE) return true;
|
||||
}
|
||||
|
||||
if (!clicked && !right_clicked)
|
||||
return true;
|
||||
if (clicked && ntp_enabled_rect().contains(g_mouse_x, g_mouse_y)) {
|
||||
g_ntp_enabled = !g_ntp_enabled;
|
||||
g_ntp_dirty = ntp_settings_changed();
|
||||
return true;
|
||||
}
|
||||
if (clicked && !g_ntp_syncing &&
|
||||
ntp_sync_button_rect().contains(g_mouse_x, g_mouse_y)) {
|
||||
synchronize_ntp();
|
||||
return true;
|
||||
}
|
||||
if (clicked && revert_button_rect().contains(g_mouse_x, g_mouse_y)) {
|
||||
if (g_ntp_dirty) revert_ntp_settings();
|
||||
return true;
|
||||
}
|
||||
if (clicked && apply_button_rect().contains(g_mouse_x, g_mouse_y)) {
|
||||
if (g_ntp_dirty) save_ntp_settings();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Rect country = country_list_rect();
|
||||
Rect zone = zone_list_rect();
|
||||
Rect revert = revert_button_rect();
|
||||
@@ -745,7 +951,6 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
}
|
||||
}
|
||||
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
if (!clicked) return redraw;
|
||||
|
||||
if (!csb_track.empty() && csb_track.contains(g_mouse_x, g_mouse_y)) {
|
||||
@@ -804,7 +1009,31 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
}
|
||||
|
||||
static bool handle_key(const montauk::abi::KeyEvent& key) {
|
||||
if (!key.pressed || g_country_count <= 0) return false;
|
||||
if (!key.pressed) return false;
|
||||
|
||||
if (key.ascii == '\t') {
|
||||
g_tab = g_tab == TAB_TIME_ZONES ? TAB_NTP : TAB_TIME_ZONES;
|
||||
mtk::context_menu_close(g_ntp_input.context);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (g_tab == TAB_NTP) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
synchronize_ntp();
|
||||
return true;
|
||||
}
|
||||
if (key.ascii == '\033') {
|
||||
if (g_ntp_dirty) revert_ntp_settings();
|
||||
return true;
|
||||
}
|
||||
int result = mtk::text_input_key(g_ntp_input, g_ntp_server,
|
||||
(int)sizeof(g_ntp_server), key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_ntp_dirty = ntp_settings_changed();
|
||||
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
|
||||
}
|
||||
|
||||
if (g_country_count <= 0) return false;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (g_dirty) save_selection();
|
||||
@@ -851,10 +1080,11 @@ extern "C" void _start() {
|
||||
load_accent();
|
||||
load_timezone_data();
|
||||
|
||||
if (!g_win.create("Time Zone", INIT_W, INIT_H))
|
||||
if (!g_win.create("Time", INIT_W, INIT_H))
|
||||
montauk::exit(1);
|
||||
|
||||
load_saved_selection();
|
||||
load_ntp_settings();
|
||||
render();
|
||||
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
@@ -864,6 +1094,11 @@ extern "C" void _start() {
|
||||
|
||||
if (r < 0) break;
|
||||
if (r == 0) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (g_tab == TAB_NTP && now - g_last_clock_render >= 1000) {
|
||||
g_last_clock_render = now;
|
||||
render();
|
||||
}
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[app]
|
||||
id = "timezone"
|
||||
name = "Time Zone"
|
||||
name = "Time"
|
||||
binary = "timezone.elf"
|
||||
icon = "preferences-system-time.svg"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user