U6: follow the shell's clock, make the date order a setting, lift Config into a shared module #17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "upgrade/u6-format"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Date and time formatting becomes configurable — but only where the shell does not
already answer the question — and
Configmoves out ofbar.luauso both entriesshare one.
Both gates green:
Measured first
A scratch plugin (
yuris/u6probe, distinct id, one[[service]]entry, torn downand proven gone below) logged the three host functions at DEBUG. Fixed epoch
1756700000=2025-09-01 11:13:20local.noctalia.timeFormat(){:%H:%M}noctalia.dateFormat()%A, %xnoctalia.formatTime(dateFormat(), E)Monday, 09/01/2025noctalia.isValidTimezone("")truenoctalia.isValidTimezone("Europe/Berlin")truenoctalia.isValidTimezone("europe/berlin")false— case-sensitivenoctalia.isValidTimezone("Not/AZone")falseDirectives, same epoch. Both the bare strftime and the
{: }chrono spelling workand give the same string:
%H:%M/{:%H:%M}11:13%d/%m/{:%d/%m}01/09%I:%M %p11:13 AM%m/%d09/01%l:%M %p11:13 AM%Y-%m-%d2025-09-01%r11:13:20 AM%a/%AMon/Monday%p/%PAM/am%x/%X09/01/2025/11:13:20 AM%R11:13%s1756700000%Q%Q— unknown passes through, no error""""Failure modes: an invalid timezone silently falls back to local
(
formatTime("%H:%M", E, "Not/AZone")→11:13, same as local;Europe/Berlin→
06:13, so the argument is live). A non-number epoch raises:invalid argument #2 to 'formatTime' (number expected, got string).One measurement that changed the design more than the rest:
25200 seconds apart, exactly this machine's UTC offset — Luau's
os.timereads atable as UTC. So there is no honest way to turn timew's local wall-clock text into
an epoch in pure Luau, which rules out handing a user pattern to
formatTimeper row.Settings added
date_format— aselectunder[[panel.setting]]:dmy(31/08, default),mdy(08/31),iso(08-31). It reaches the history list's lead column, the daypicker's dated label (
Sat 08/29), and the row menu's header.Justified because
dateFormat()does not answer it.%A, %xrendersMonday, 09/01/2025— a calendar heading.LEAD_Wis 88px and budgets31/08 20:22; that string would clip rather than inform. What is left of%xthatis useful is the field order, and a US reader seeing
01/09for September 1st readsJanuary 9th. One setting, three values, and the values are the keys of
Fmt.DATE_ORDERS—Config'sCONFIG_OPTIONSis that same table rather than asecond list, so accepted and implemented cannot drift.
Settings rejected
12-hour / 24-hour — rejected, and following the shell instead.
timeFormat()hands over the user's own
[shell].time_format. The shell already owns this for thewhole desktop; a copy under Settings → Plugins would only let one user disagree with
themselves.
Config.hour12reads the pattern for the five directives that mean12-hour (
%I %l %r %p %P) and the panel follows it, so a user on{:%I:%M %p}nowsees
5:48 PMhere with no knob touched.Only the 12/24 bit of the pattern is taken, not the pattern itself, and that is
deliberate: applying the whole thing needs an epoch that (per the
os.timemeasurement above) cannot be reconstructed honestly, and
%A, %xin an 88px columnclips. The conversion is arithmetic on the
HH:MMtimew already printed.Timezone — rejected. timew reports an interval's start as local wall-clock text
with no zone. There is no instant to re-project, and manufacturing one via
os.timewould shift every row by the UTC offset. A timezone setting here could only make the
panel lie about when the work happened.
isValidTimezonewas measured anyway (above)so the rejection rests on knowing what it does, not on not having tried.
Today / Yesterday — rejected, they stay unconditional. Strictly more informative
than the date they replace; they render in one place, the scope button, which is not a
column anything lines up with; and the chart under them already says which day is which
by position. A knob there only lets a user make the button say less.
A free-text strftime pattern — rejected in favour of the select. It would need the
host and an epoch (see above), it would put the pure
Fmtmodule below a host call orforce a hand-written mini-strftime, and it adds a whole class of failure — a nonsense
pattern — for a want the three orders already cover. The select still has to survive
nonsense, because a hand-edited
settings.tomlis not the dropdown.Bad-value fallback
Every case degrades to the default and none breaks the panel. One line each:
date_format = "banana"dmytest.luau:788, and on the rendered tree at:2828date_format = "DMY"dmytest.luau:789date_format = "dmy "dmytest.luau:790date_format = ""dmytest.luau:791, tree at:2832date_format = nildmytest.luau:792, tree at:2836date_format = 7/truedmytest.luau:793-794icon_size = 99/128/10test.luau:746-747(existing, still green)icon_size = "big"17test.luau:748[shell].time_format = "{:%A}"test.luau:834[shell].time_format = ""test.luau:833HH:MM"9:00","--:--","1748",""test.luau:217-219Confirmed live as well as in the suite: the plugin was loaded into the running shell
under a distinct id with
date_format's manifest default seeded as each ofmdy,iso,bananaand""in turn, the panel opened and switched to all-time scope oneach, and the log carried no script error in any of the four.
Mutation results
14 mutations, each applied to a source file, suite run, file restored, sha256 compared,
suite run again. Every one caught; every restore byte-identical; green after each.
Fmt.DATE_ORDERS.dmyday/month swapped:157 index 3 is dow plus the dated form, verbatimFmt.hhmmmidnight:0no longer becomes12:206 midnight is 12 AM, not 0 AMFmt.hhmmboundary< 12→<= 12:207 noon is 12 PM, not 0 PMConfig's select-options guard disabled:788 a value outside the manifest's options is the default%p/%Pdropped fromhour12's class:833 a marker with a 24-hour hour is still the user asking for onehour12given a real cache:830 %I:%M %p renders 11:13 AMformatOptshardcodesdateOrder = "dmy":2818 mdy reaches the list: "31/08 13:00|…"Config.invalidate()dropped from panelonConfigChanged:2891 onConfigChanged dropped the cache and repainted…row.date .. " " .. row.at:2701 the header names row 2's own date…: "2026-08-31 12:00 · admin · 0:30"Config.DEFAULTS.date_formata literal, notFmt.DATE_DEFAULT:788 a value outside the manifest's options is the defaultFmt.DATE_ORDERS.isoseparator-→/:173 iso is month first, hyphenatedFmt.datefallback removed (indexes an unknown order)fmt.luau:64 attempt to index nil with numberAM/PMinstead oftr-ing them:2867 the markers are tr keys, not literalsFmt.hhmmignores the meridiemfmt.luau:90 attempt to index nil with 'pm'Mutation 6 is worth a note: the first attempt at it added an early return but never
populated the cache, so it was a no-op and survived. It was rewritten to cache for
real and then failed at
:830. Mutation 6 also retired an assert — the trailing"not cached" pair at the end of the
hour12block could not be made to failindependently of
:830, so it was deleted rather than kept as decoration.The
ConfigliftConfigmoves frombar.luautotimew/config.luau(plusconfig.luau.luau, theCLI alias every module needs). One
DEFAULTS, oneCONFIG_BOUNDS, one cache, theexisting coercion and clamp, and
invalidate()ononConfigChanged— behaviourunchanged, and the existing asserts at
test.luau:745-776still cover it. New besidethem:
CONFIG_OPTIONS, which is the select check, andConfig.hour12.Each entry is a separate Luau VM with its own module cache, so the shell loads one
instance per entry and each gets its own
configCache— the sharing is of source, notof state. That is in a comment at the top of the file. The bar reads
tagandicon_size; the panel readsdate_format; a key an entry never asks for is neverfetched in that VM.
hour12is deliberately not cached.[shell].time_formatis not a plugin setting,so changing it dispatches no
onConfigChanged(script_runtime.cpp:687-695) and acached answer would sit stale until the VM was recycled. It costs one mutex-guarded
string copy per repaint.
Other changes worth seeing
Parse.intervalnow returns timew's rawdate = "YYYY-MM-DD"instead of apre-sliced
"31/08". Slicing it in the parser throws the choice away, and it left thepanel with two date shapes — the parser's
dd/mmandTimew.parseDayTotals'YYYY-MM-DDthatFmt.dayLabelalready took. One shape below the formatter now.That forced the one line in the row context menu (
{ when = … }), which is otherwiseuntouched: with a raw
datethe old concatenation would have printed an ISO date undera panel set to anything else. Gestures and
README.mdare untouched.bar.luauexportselapsednow.getfenv(bar.Config.get)was how the suite reachedbar's globals table, and the lift took that away —
Configbelongs to the module now.elapsedis the one functionbar.luaustill defines itself.The suite's host trap (
test.luau:2107-2124) changed subject: it now trapsViewmodel.buildrather thanpanel.viewModel, becauseviewModelcallsformatOptsand formatting genuinely needs the host. The pair of asserts under it is the point —
the first says the model asks the host nothing, the second says
formatOptsdoes,which is what stops the first from passing on a panel that formats nothing.
Scratch teardown
Log level was set back to
info. The enabledyuris/timewwas never touched andsettings.tomlwas never hand-edited.What a reviewer should challenge
timeFormat(). A user whose format is{:%Hh%M}gets
17:48, not17h48. Defensible on the epoch measurement and the column width,but it is a judgement call and the alternative is a per-row
formatTimeon an epochreconstructed from the UTC offset.
Fmt.hhmmtakes{ am, pm }from the caller because it ispure. That means
panel.am/panel.pmare this plugin's strings, not the locale's%p— a Spanish user with an English plugin locale getsAM, whereformatTimewould have given them the C locale's answer.
isoas08-31. Month-day, no year, hyphenated. It is ISO field order, not anISO date, and the label says
ISO — 08-31to admit that. A reviewer may want itnamed
ymdor dropped.[[panel.setting]]versus plugin-level. Scoped to the panel because the bar showsno date. Both edit under the same gear, so nothing is lost — but if a future bar
tooltip wants a date, this has to move.
Parse.interval's shape change ripples further than the diff suggests: anythingreading
row.dateexpectingdd/mmis now wrong. The suite covers the three readersin the repo; a fourth would not be caught by the type system.
The panel wrote every date as dd/mm and every time as whatever timew printed, both spelled inline. Two things were wrong with that: dd/mm is a guess about the reader, and the shell already holds a 12h/24h preference this plugin was ignoring. Measured first (scratch plugin, torn down): noctalia.timeFormat() -> "{:%H:%M}" the user's own [shell].time_format noctalia.dateFormat() -> "%A, %x" renders "Monday, 09/01/2025" noctalia.isValidTimezone("") -> true, "Europe/Berlin" -> true, "europe/berlin" -> false, "Not/AZone" -> false So 12h/24h is already answered shell-wide and a plugin setting beside it would be a bug: Config.hour12 reads timeFormat() and the panel follows it. There is one new setting, `date_format`, because dateFormat() answers a different question (a calendar heading, not a lead column). No timezone setting: timew reports local wall-clock text with no zone, so there is no instant to project. Config moves out of bar.luau into ./config.luau, shared by both entries -- one DEFAULTS, one coercion, one clamp, one cache, plus a select-options check for the new key. Each entry is its own VM and so gets its own cache instance. Parse.interval now hands over timew's raw "YYYY-MM-DD"; Fmt.date orders it and Fmt.when is the one spelling of "date then time" that both the all-time list and the row menu's header go through.Verdict: APPROVE
Reviewed by the integrator. The measurements are the valuable part of this PR and I reproduced
the load-bearing one.
Verified
The
os.timefinding is real and subtle. Reproduced on this machine:Luau's
os.timereads the table as UTC whileos.daterenders local, so timew's localwall-clock text cannot be turned into an honest epoch in pure Luau. That correctly rules out
per-row
formatTimewith a user pattern, and it is the kind of thing that would have shippedas a seven-hour bug in one timezone.
Three rejections, all better than accepting. 12h/24h is already answered shell-wide by
timeFormat(), andConfig.hour12reading%I %l %r %p %Pout of the user's own pattern isthe right shape - a duplicate setting would let one user disagree with themselves. The timezone
knob could only make the panel lie, given the above. Today/Yesterday is strictly more
informative than the date it replaces.
One setting added, and it earns its place.
dateFormat()returns%A, %x-> "Monday,09/01/2025", which clips an 88px lead column, so the field order genuinely is unanswered - and
01/09reads as 9 January to a US reader. Options are the keys ofFmt.DATE_ORDERSandCONFIG_OPTIONSis that same table, so accepted and implemented cannot drift.Gates:
12 states, 1739 field comparisons / ok;0 errors, 0 warnings. Probe torn downclean: 0 in
plugins list, 0 insettings.toml.config.luauhas its.luau.luaualias.Mutations
mdyaliasesdmyPlus the author's own 14/14. I could not construct a survivor against the new setting.
Advisory
The eleven bad-value cases are the strongest part of the test work, and the live check - loading
the plugin four times with
banana,'',mdyandisoseeded as the manifest default - isbetter evidence than the unit asserts alone. Worth keeping that method for the next setting.