diff --git a/plugins/dclock.c b/plugins/dclock.c index 1a7fe187..2b87a287 100644 --- a/plugins/dclock.c +++ b/plugins/dclock.c @@ -169,6 +169,202 @@ static void dclock_timer_set(DClockPlugin * dc, struct timeval *current_time) dc->timer = g_timeout_add(milliseconds, (GSourceFunc) dclock_update_display, (gpointer) dc); } +/* Erisian (Discordian) calendar support, ported from dtime-js + * (https://gitlab.com/zlax/dtime-js). + * A year has five 73-day seasons plus St. Tib's Day in leap years. + * A day has ten decimal hours, each a hundred decimal minutes of a hundred + * decimal seconds (one decimal second equals 864 christian milliseconds). + */ +typedef struct { + int yold; /* Erisian year (YOLD) */ + int season; /* 0-4 */ + int day; /* 1-73 within the season */ + int hour; /* 0-9 decimal hour */ + int minute; /* 0-99 decimal minute */ + int second; /* 0-99 decimal second */ + int st_tibs; /* 1 on St. Tib's Day */ + char season_name[16]; /* e.g. "Bureaucracy" */ + char weekday_name[32]; /* one of five */ +} DClockErisTime; + +/* Easter Island winter time offset used by dtime-js, in seconds. */ +#define DCLOCK_ERIS_TZ_OFFSET (-5 * 60 * 60) + +static const char *dclock_eris_seasons[5] = { + "Chaos", "Discord", "Confusion", "Bureaucracy", "Aftermath" +}; + +static const char *dclock_eris_weekdays[5] = { + "Sweetmorn", "Boomtime", "Pungenday", "Prickle-Prickle", "Setting Orange" +}; + +/* Cumulative days before each month (January = index 0); index 12 = total + * days in the year. Turns a Gregorian date into a day-of-year by lookup. */ +static const unsigned short dclock_eris_cum_common[13] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 +}; +static const unsigned short dclock_eris_cum_leap[13] = { + 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 +}; + +static int dclock_eris_leap(int year) +{ + return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); +} + +/* Day-of-year (1-based) from a Gregorian date, via the cumulative-day table. */ +static int dclock_eris_day_of_year(int day, int month, int year) +{ + const unsigned short * cum = dclock_eris_leap(year) + ? dclock_eris_cum_leap + : dclock_eris_cum_common; + return (int)(cum[month - 1] + day); +} + +/* Cached Erisian "day anchor". The Erisian date depends only on the (offset) + * UTC calendar day, so it changes at most once per day. This structure is + * refreshed only when that day rolls over; between refreshes the decimal time + * is derived cheaply (a subtraction from day_start, no modulo). */ +typedef struct { + int64_t day_start; /* shifted unix sec of 00:00:00 of the current day */ + int64_t next; /* shifted unix sec at which the date changes */ + int yold, season, day, st_tibs; + char weekday_name[32]; + int valid; +} DClockErisDay; + +static DClockErisDay eris_day = { 0 }; + +static void dclock_eris_now(const struct timeval *now, DClockErisTime *e); +static char *dclock_expand_codes(const char *fmt, const DClockErisTime *eris, + long long unix_sec); + +/* Compute the Erisian date (cached; refreshed only when the shifted UTC day + * changes) and the decimal time (every call) for the current moment. */ +static void dclock_eris_now(const struct timeval *now, DClockErisTime *e) +{ + time_t shifted_sec = (time_t)now->tv_sec + DCLOCK_ERIS_TZ_OFFSET; + + /* Refresh the cached Erisian date only when the shifted UTC day changes — + * at most once per day. The recompute branch is rare, so mark it unlikely + * to keep the common cache-hit path lean. */ + if (!eris_day.valid || __builtin_expect(shifted_sec >= eris_day.next, 0)) { + struct tm utc; + int dom, mon, year, doy, st_tibs; + int season, day, yold, sec_since_midnight; + + gmtime_r(&shifted_sec, &utc); + dom = utc.tm_mday; + mon = utc.tm_mon + 1; + year = utc.tm_year + 1900; + + doy = dclock_eris_day_of_year(dom, mon, year); + st_tibs = dclock_eris_leap(year) && (doy == 60); + if (dclock_eris_leap(year) && doy > 60) + doy--; + season = doy / 73; + day = doy % 73; + if (day == 0) { + season--; + day = 73; + } + yold = year + 1166; + + /* Anchor of the current shifted UTC day (00:00:00) and its end. */ + sec_since_midnight = utc.tm_hour * 3600 + utc.tm_min * 60 + utc.tm_sec; + eris_day.day_start = (int64_t)shifted_sec - sec_since_midnight; + eris_day.next = eris_day.day_start + 86400; + + eris_day.yold = yold; + eris_day.season = season; + eris_day.day = day; + eris_day.st_tibs = st_tibs; + if (st_tibs) + eris_day.weekday_name[0] = '\0'; + else + snprintf(eris_day.weekday_name, sizeof(eris_day.weekday_name), + "%s", dclock_eris_weekdays[(doy - 1) % 5]); + eris_day.valid = 1; + } + + e->yold = eris_day.yold; + e->season = eris_day.season; + e->day = eris_day.day; + e->st_tibs = eris_day.st_tibs; + snprintf(e->season_name, sizeof(e->season_name), "%s", + dclock_eris_seasons[eris_day.season]); + memcpy(e->weekday_name, eris_day.weekday_name, sizeof(e->weekday_name)); + + /* Decimal time: seconds (and ms) into the current day, derived from the + * cached day_start by subtraction (no modulo). ds = decimal-second 0-99999. */ + int64_t secs_into_day = (int64_t)shifted_sec - eris_day.day_start; + int64_t ms_into_day = secs_into_day * 1000 + (int64_t)(now->tv_usec / 1000); + int64_t ds = ms_into_day / 864; + e->hour = (int)(ds / 10000); + e->minute = (int)((ds % 10000) / 100); + e->second = (int)(ds % 100); +} + +/* Expand the non-strftime codes (%e* and %ut) in a format string, leaving the + * remaining % codes for strftime(). Returns a newly allocated string. */ +static char *dclock_expand_codes(const char *fmt, const DClockErisTime *eris, + long long unix_sec) +{ + GString *s = g_string_new(NULL); + const char *p; + + if (fmt == NULL) + return g_string_free(s, FALSE); + + for (p = fmt; *p; ) { + int advanced = 0; + if (*p == '%') { + if (p[1] == '%') { + g_string_append(s, "%%"); /* literal % — leave for strftime() */ + advanced = 2; + } + else if (p[1] == 'e') { + switch (p[2]) { + case 'Y': g_string_append_printf(s, "%d", eris->yold); advanced = 3; break; + case 'y': g_string_append_printf(s, "%d", eris->yold - 3000); advanced = 3; break; + case 'S': g_string_append(s, eris->season_name); advanced = 3; break; + case 'E': g_string_append_printf(s, "%d", eris->season + 1); advanced = 3; break; + case 'd': + if (eris->st_tibs) + g_string_append(s, "tib"); + else + g_string_append_printf(s, "%d", eris->day); + advanced = 3; + break; + case 'h': g_string_append_printf(s, "%d", eris->hour); advanced = 3; break; + case 'm': g_string_append_printf(s, "%02d", eris->minute); advanced = 3; break; + case 's': g_string_append_printf(s, "%02d", eris->second); advanced = 3; break; + case 'w': + if (eris->st_tibs) + g_string_append(s, "St. Tib's Day"); + else + g_string_append(s, eris->weekday_name); + advanced = 3; + break; + default: break; + } + } + else if (p[1] == 'u' && p[2] == 't') { + g_string_append_printf(s, "%lld", unix_sec); + advanced = 3; + } + } + if (advanced) { + p += advanced; + continue; + } + g_string_append_c(s, *p); + p++; + } + + return g_string_free(s, FALSE); +} + /* Periodic timer callback. * Also used during initialization and configuration change to do a redraw. */ static gboolean dclock_update_display(DClockPlugin * dc) @@ -183,15 +379,24 @@ static gboolean dclock_update_display(DClockPlugin * dc) dclock_timer_set(dc, &now); current_time = localtime(&now.tv_sec); - /* Determine the content of the clock label and tooltip. */ - char clock_value[64]; - char tooltip_value[64]; + /* Determine the content of the clock label and tooltip. + * Compute the Erisian date/time once (cached date, cheap decimal time), then + * expand the Erisian/unix codes in both formats before strftime. */ + DClockErisTime eris; + dclock_eris_now(&now, &eris); + char * expanded_clock = dclock_expand_codes(dc->clock_format, &eris, (long long)now.tv_sec); + char * expanded_tooltip = dclock_expand_codes(dc->tooltip_format, &eris, (long long)now.tv_sec); + + char clock_value[128]; + char tooltip_value[128]; clock_value[0] = '\0'; if (dc->clock_format != NULL) - strftime(clock_value, sizeof(clock_value), dc->clock_format, current_time); + strftime(clock_value, sizeof(clock_value), expanded_clock, current_time); tooltip_value[0] = '\0'; if (dc->tooltip_format != NULL) - strftime(tooltip_value, sizeof(tooltip_value), dc->tooltip_format, current_time); + strftime(tooltip_value, sizeof(tooltip_value), expanded_tooltip, current_time); + g_free(expanded_clock); + g_free(expanded_tooltip); /* When we write the clock value, it causes the panel to do a full relayout. * Since this function may be called too often while the timing experiment is underway, @@ -435,6 +640,9 @@ static GtkWidget *dclock_configure(LXPanel *panel, GtkWidget *p) _("Clock Format"), &dc->clock_format, CONF_TYPE_STR, _("Tooltip Format"), &dc->tooltip_format, CONF_TYPE_STR, _("Format codes: man 3 strftime; %n for line break"), NULL, CONF_TYPE_TRIM, + _("Erisian/unix: %eY year %ey short-year %eS season %eE season# %ed day " + "%eh:%em:%es hour:min:sec %ew weekday (St Tib's day: %ed=tib, %ew=St. Tib's Day) " + "%ut unix time"), NULL, CONF_TYPE_TRIM, _("Action when clicked (default: display calendar)"), &dc->action, CONF_TYPE_STR, _("Bold font"), &dc->bold, CONF_TYPE_BOOL, _("Tooltip only"), &dc->icon_only, CONF_TYPE_BOOL, diff --git a/po/lxpanel.pot b/po/lxpanel.pot index dc6b1b07..e6e86067 100644 --- a/po/lxpanel.pot +++ b/po/lxpanel.pot @@ -716,6 +716,12 @@ msgstr "" msgid "Format codes: man 3 strftime; %n for line break" msgstr "" +#: ../plugins/dclock.c +msgid "Erisian/unix: %eY year %ey short-year %eS season %eE season# %ed day " +"%eh:%em:%es hour:min:sec %ew weekday (St Tib's day: %ed=tib, %ew=St. Tib's Day) " +"%ut unix time" +msgstr "" + #: ../plugins/dclock.c:438 msgid "Action when clicked (default: display calendar)" msgstr ""