U6: follow the shell's clock, make the date order a setting, lift Config into a shared module #17

Merged
yuris merged 1 commit from upgrade/u6-format into main 2026-09-01 20:21:28 +00:00
Owner

Date and time formatting becomes configurable — but only where the shell does not
already answer the question — and Config moves out of bar.luau so both entries
share one.

Both gates green:

$ nix shell nixpkgs#luau -c luau test.luau
viewModel: 12 states, 1739 field comparisons
ok

$ noctalia plugins lint timew
yuris/timew
  ok

0 errors, 0 warnings

Measured first

A scratch plugin (yuris/u6probe, distinct id, one [[service]] entry, torn down
and proven gone below) logged the three host functions at DEBUG. Fixed epoch
1756700000 = 2025-09-01 11:13:20 local.

Call Result
noctalia.timeFormat() {:%H:%M}
noctalia.dateFormat() %A, %x
noctalia.formatTime(dateFormat(), E) Monday, 09/01/2025
noctalia.isValidTimezone("") true
noctalia.isValidTimezone("Europe/Berlin") true
noctalia.isValidTimezone("europe/berlin") false — case-sensitive
noctalia.isValidTimezone("Not/AZone") false

Directives, same epoch. Both the bare strftime and the {: } chrono spelling work
and give the same string:

Pattern Renders Pattern Renders
%H:%M / {:%H:%M} 11:13 %d/%m / {:%d/%m} 01/09
%I:%M %p 11:13 AM %m/%d 09/01
%l:%M %p 11:13 AM %Y-%m-%d 2025-09-01
%r 11:13:20 AM %a / %A Mon / Monday
%p / %P AM / am %x / %X 09/01/2025 / 11:13:20 AM
%R 11:13 %s 1756700000
%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:

os.date("%H:%M", 1756700000)                          -> 11:13
os.time{year=2025,month=9,day=1,hour=11,min=13,sec=20} -> 1756725200

25200 seconds apart, exactly this machine's UTC offset — Luau's os.time reads a
table 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 formatTime per row.

Settings added

date_format — a select under [[panel.setting]]: dmy (31/08, default),
mdy (08/31), iso (08-31). It reaches the history list's lead column, the day
picker's dated label (Sat 08/29), and the row menu's header.

Justified because dateFormat() does not answer it. %A, %x renders
Monday, 09/01/2025 — a calendar heading. LEAD_W is 88px and budgets
31/08 20:22; that string would clip rather than inform. What is left of %x that
is useful is the field order, and a US reader seeing 01/09 for September 1st reads
January 9th. One setting, three values, and the values are the keys of
Fmt.DATE_ORDERS — Config's CONFIG_OPTIONS is that same table rather than a
second 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 the
whole desktop; a copy under Settings → Plugins would only let one user disagree with
themselves. Config.hour12 reads the pattern for the five directives that mean
12-hour (%I %l %r %p %P) and the panel follows it, so a user on {:%I:%M %p} now
sees 5:48 PM here 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.time
measurement above) cannot be reconstructed honestly, and %A, %x in an 88px column
clips. The conversion is arithmetic on the HH:MM timew 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.time
would shift every row by the UTC offset. A timezone setting here could only make the
panel lie about when the work happened. isValidTimezone was 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 Fmt module below a host call or
force 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.toml is not the dropdown.

Bad-value fallback

Every case degrades to the default and none breaks the panel. One line each:

Case Stored Result Where
nonsense select value date_format = "banana" dmy test.luau:788, and on the rendered tree at :2828
wrong case date_format = "DMY" dmy test.luau:789
trailing space date_format = "dmy " dmy test.luau:790
empty string date_format = "" dmy test.luau:791, tree at :2832
absent date_format = nil dmy test.luau:792, tree at :2836
wrong type date_format = 7 / true dmy test.luau:793-794
out-of-bounds number icon_size = 99 / 1 28 / 10 test.luau:746-747 (existing, still green)
uncoercible number icon_size = "big" 17 test.luau:748
nonsense time pattern [shell].time_format = "{:%A}" 24-hour, no marker test.luau:834
empty time pattern [shell].time_format = "" 24-hour test.luau:833
unparseable HH:MM "9:00", "--:--", "1748", "" handed back verbatim test.luau:217-219
invalid timezone not a setting n/a — see the rejection above —

Confirmed 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 of mdy,
iso, banana and "" in turn, the panel opened and switched to all-time scope on
each, 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.

# Mutation Failing assert
1 Fmt.DATE_ORDERS.dmy day/month swapped :157 index 3 is dow plus the dated form, verbatim
2 Fmt.hhmm midnight: 0 no longer becomes 12 :206 midnight is 12 AM, not 0 AM
3 Fmt.hhmm boundary < 12 → <= 12 :207 noon is 12 PM, not 0 PM
4 Config's select-options guard disabled :788 a value outside the manifest's options is the default
5 %p/%P dropped from hour12's class :833 a marker with a 24-hour hour is still the user asking for one
6 hour12 given a real cache :830 %I:%M %p renders 11:13 AM
7 formatOpts hardcodes dateOrder = "dmy" :2818 mdy reaches the list: "31/08 13:00|…"
8 Config.invalidate() dropped from panel onConfigChanged :2891 onConfigChanged dropped the cache and repainted…
9 row menu header back to row.date .. " " .. row.at :2701 the header names row 2's own date…: "2026-08-31 12:00 · admin · 0:30"
10 Config.DEFAULTS.date_format a literal, not Fmt.DATE_DEFAULT :788 a value outside the manifest's options is the default
11 Fmt.DATE_ORDERS.iso separator - → / :173 iso is month first, hyphenated
12 Fmt.date fallback removed (indexes an unknown order) fmt.luau:64 attempt to index nil with number
13 panel spells AM/PM instead of tr-ing them :2867 the markers are tr keys, not literals
14 Fmt.hhmm ignores the meridiem fmt.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 hour12 block could not be made to fail
independently of :830, so it was deleted rather than kept as decoration.

The Config lift

Config moves from bar.luau to timew/config.luau (plus config.luau.luau, the
CLI alias every module needs). One DEFAULTS, one CONFIG_BOUNDS, one cache, the
existing coercion and clamp, and invalidate() on onConfigChanged — behaviour
unchanged, and the existing asserts at test.luau:745-776 still cover it. New beside
them: CONFIG_OPTIONS, which is the select check, and Config.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, not
of state. That is in a comment at the top of the file. The bar reads tag and
icon_size; the panel reads date_format; a key an entry never asks for is never
fetched in that VM.

hour12 is deliberately not cached. [shell].time_format is not a plugin setting,
so changing it dispatches no onConfigChanged (script_runtime.cpp:687-695) and a
cached answer would sit stale until the VM was recycled. It costs one mutex-guarded
string copy per repaint.

Other changes worth seeing

Parse.interval now returns timew's raw date = "YYYY-MM-DD" instead of a
pre-sliced "31/08". Slicing it in the parser throws the choice away, and it left the
panel with two date shapes — the parser's dd/mm and Timew.parseDayTotals'
YYYY-MM-DD that Fmt.dayLabel already took. One shape below the formatter now.

That forced the one line in the row context menu ({ when = … }), which is otherwise
untouched: with a raw date the old concatenation would have printed an ISO date under
a panel set to anything else. Gestures and README.md are untouched.

bar.luau exports elapsed now. getfenv(bar.Config.get) was how the suite reached
bar's globals table, and the lift took that away — Config belongs to the module now.
elapsed is the one function bar.luau still defines itself.

The suite's host trap (test.luau:2107-2124) changed subject: it now traps
Viewmodel.build rather than panel.viewModel, because viewModel calls formatOpts
and 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 formatOpts does,
which is what stops the first from passing on a panel that formats nothing.

Scratch teardown

$ noctalia msg plugins list | grep -c u6probe
0
$ noctalia msg plugins list | grep -c u6live
0
$ grep -cE 'u6live|u6probe' ~/.local/state/noctalia/settings.toml
0
$ noctalia msg plugins source list
official git https://github.com/noctalia-dev/official-plugins
community git https://github.com/noctalia-dev/community-plugins
my-dev path /home/yuris/Development/other/noctalia-timew
$ ls -d /tmp/u6*
ls: cannot access '/tmp/u6*': No such file or directory
$ noctalia msg plugins list | grep timew
yuris/timew [my-dev] 1.1.0 enabled requires timewarrior

Log level was set back to info. The enabled yuris/timew was never touched and
settings.toml was never hand-edited.

What a reviewer should challenge

  • The narrowing to one bit of timeFormat(). A user whose format is {:%Hh%M}
    gets 17:48, not 17h48. Defensible on the epoch measurement and the column width,
    but it is a judgement call and the alternative is a per-row formatTime on an epoch
    reconstructed from the UTC offset.
  • Marker translation. Fmt.hhmm takes { am, pm } from the caller because it is
    pure. That means panel.am / panel.pm are this plugin's strings, not the locale's
    %p — a Spanish user with an English plugin locale gets AM, where formatTime
    would have given them the C locale's answer.
  • iso as 08-31. Month-day, no year, hyphenated. It is ISO field order, not an
    ISO date, and the label says ISO — 08-31 to admit that. A reviewer may want it
    named ymd or dropped.
  • [[panel.setting]] versus plugin-level. Scoped to the panel because the bar shows
    no 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: anything
    reading row.date expecting dd/mm is now wrong. The suite covers the three readers
    in the repo; a fourth would not be caught by the type system.
Date and time formatting becomes configurable — but only where the shell does not already answer the question — and `Config` moves out of `bar.luau` so both entries share one. Both gates green: ``` $ nix shell nixpkgs#luau -c luau test.luau viewModel: 12 states, 1739 field comparisons ok $ noctalia plugins lint timew yuris/timew ok 0 errors, 0 warnings ``` ## Measured first A scratch plugin (`yuris/u6probe`, distinct id, one `[[service]]` entry, torn down and proven gone below) logged the three host functions at DEBUG. Fixed epoch `1756700000` = `2025-09-01 11:13:20` local. | Call | Result | |---|---| | `noctalia.timeFormat()` | `{:%H:%M}` | | `noctalia.dateFormat()` | `%A, %x` | | `noctalia.formatTime(dateFormat(), E)` | `Monday, 09/01/2025` | | `noctalia.isValidTimezone("")` | `true` | | `noctalia.isValidTimezone("Europe/Berlin")` | `true` | | `noctalia.isValidTimezone("europe/berlin")` | `false` — case-sensitive | | `noctalia.isValidTimezone("Not/AZone")` | `false` | Directives, same epoch. Both the bare strftime and the `{: }` chrono spelling work and give the same string: | Pattern | Renders | | Pattern | Renders | |---|---|---|---|---| | `%H:%M` / `{:%H:%M}` | `11:13` | | `%d/%m` / `{:%d/%m}` | `01/09` | | `%I:%M %p` | `11:13 AM` | | `%m/%d` | `09/01` | | `%l:%M %p` | `11:13 AM` | | `%Y-%m-%d` | `2025-09-01` | | `%r` | `11:13:20 AM` | | `%a` / `%A` | `Mon` / `Monday` | | `%p` / `%P` | `AM` / `am` | | `%x` / `%X` | `09/01/2025` / `11:13:20 AM` | | `%R` | `11:13` | | `%s` | `1756700000` | | `%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: ``` os.date("%H:%M", 1756700000) -> 11:13 os.time{year=2025,month=9,day=1,hour=11,min=13,sec=20} -> 1756725200 ``` 25200 seconds apart, exactly this machine's UTC offset — **Luau's `os.time` reads a table 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 `formatTime` per row. ## Settings added **`date_format`** — a `select` under `[[panel.setting]]`: `dmy` (`31/08`, default), `mdy` (`08/31`), `iso` (`08-31`). It reaches the history list's lead column, the day picker's dated label (`Sat 08/29`), and the row menu's header. Justified because `dateFormat()` does not answer it. `%A, %x` renders `Monday, 09/01/2025` — a calendar heading. `LEAD_W` is 88px and budgets `31/08 20:22`; that string would clip rather than inform. What is left of `%x` that *is* useful is the field order, and a US reader seeing `01/09` for September 1st reads January 9th. One setting, three values, and the values are the keys of `Fmt.DATE_ORDERS` — `Config`'s `CONFIG_OPTIONS` is that same table rather than a second 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 the whole desktop; a copy under Settings → Plugins would only let one user disagree with themselves. `Config.hour12` reads the pattern for the five directives that mean 12-hour (`%I %l %r %p %P`) and the panel follows it, so a user on `{:%I:%M %p}` now sees `5:48 PM` here 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.time` measurement above) cannot be reconstructed honestly, and `%A, %x` in an 88px column clips. The conversion is arithmetic on the `HH:MM` timew 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.time` would shift every row by the UTC offset. A timezone setting here could only make the panel lie about when the work happened. `isValidTimezone` was 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 `Fmt` module below a host call or force 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.toml` is not the dropdown. ## Bad-value fallback Every case degrades to the default and none breaks the panel. One line each: | Case | Stored | Result | Where | |---|---|---|---| | nonsense select value | `date_format = "banana"` | `dmy` | `test.luau:788`, and on the rendered tree at `:2828` | | wrong case | `date_format = "DMY"` | `dmy` | `test.luau:789` | | trailing space | `date_format = "dmy "` | `dmy` | `test.luau:790` | | empty string | `date_format = ""` | `dmy` | `test.luau:791`, tree at `:2832` | | absent | `date_format = nil` | `dmy` | `test.luau:792`, tree at `:2836` | | wrong type | `date_format = 7` / `true` | `dmy` | `test.luau:793-794` | | out-of-bounds number | `icon_size = 99` / `1` | `28` / `10` | `test.luau:746-747` (existing, still green) | | uncoercible number | `icon_size = "big"` | `17` | `test.luau:748` | | nonsense time pattern | `[shell].time_format = "{:%A}"` | 24-hour, no marker | `test.luau:834` | | empty time pattern | `[shell].time_format = ""` | 24-hour | `test.luau:833` | | unparseable `HH:MM` | `"9:00"`, `"--:--"`, `"1748"`, `""` | handed back verbatim | `test.luau:217-219` | | invalid timezone | *not a setting* | n/a — see the rejection above | — | Confirmed 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 of `mdy`, `iso`, `banana` and `""` in turn, the panel opened and switched to all-time scope on each, 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. | # | Mutation | Failing assert | |---|---|---| | 1 | `Fmt.DATE_ORDERS.dmy` day/month swapped | `:157 index 3 is dow plus the dated form, verbatim` | | 2 | `Fmt.hhmm` midnight: `0` no longer becomes `12` | `:206 midnight is 12 AM, not 0 AM` | | 3 | `Fmt.hhmm` boundary `< 12` → `<= 12` | `:207 noon is 12 PM, not 0 PM` | | 4 | `Config`'s select-options guard disabled | `:788 a value outside the manifest's options is the default` | | 5 | `%p`/`%P` dropped from `hour12`'s class | `:833 a marker with a 24-hour hour is still the user asking for one` | | 6 | `hour12` given a real cache | `:830 %I:%M %p renders 11:13 AM` | | 7 | `formatOpts` hardcodes `dateOrder = "dmy"` | `:2818 mdy reaches the list: "31/08 13:00\|…"` | | 8 | `Config.invalidate()` dropped from panel `onConfigChanged` | `:2891 onConfigChanged dropped the cache and repainted…` | | 9 | row menu header back to `row.date .. " " .. row.at` | `:2701 the header names row 2's own date…: "2026-08-31 12:00 · admin · 0:30"` | | 10 | `Config.DEFAULTS.date_format` a literal, not `Fmt.DATE_DEFAULT` | `:788 a value outside the manifest's options is the default` | | 11 | `Fmt.DATE_ORDERS.iso` separator `-` → `/` | `:173 iso is month first, hyphenated` | | 12 | `Fmt.date` fallback removed (indexes an unknown order) | `fmt.luau:64 attempt to index nil with number` | | 13 | panel spells `AM`/`PM` instead of `tr`-ing them | `:2867 the markers are tr keys, not literals` | | 14 | `Fmt.hhmm` ignores the meridiem | `fmt.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 `hour12` block could not be made to fail independently of `:830`, so it was deleted rather than kept as decoration. ## The `Config` lift `Config` moves from `bar.luau` to `timew/config.luau` (plus `config.luau.luau`, the CLI alias every module needs). One `DEFAULTS`, one `CONFIG_BOUNDS`, one cache, the existing coercion and clamp, and `invalidate()` on `onConfigChanged` — behaviour unchanged, and the existing asserts at `test.luau:745-776` still cover it. New beside them: `CONFIG_OPTIONS`, which is the select check, and `Config.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, not of state. That is in a comment at the top of the file. The bar reads `tag` and `icon_size`; the panel reads `date_format`; a key an entry never asks for is never fetched in that VM. `hour12` is deliberately **not** cached. `[shell].time_format` is not a plugin setting, so changing it dispatches no `onConfigChanged` (`script_runtime.cpp:687-695`) and a cached answer would sit stale until the VM was recycled. It costs one mutex-guarded string copy per repaint. ## Other changes worth seeing `Parse.interval` now returns timew's raw `date = "YYYY-MM-DD"` instead of a pre-sliced `"31/08"`. Slicing it in the parser throws the choice away, and it left the panel with two date shapes — the parser's `dd/mm` and `Timew.parseDayTotals`' `YYYY-MM-DD` that `Fmt.dayLabel` already took. One shape below the formatter now. That forced the one line in the row context menu (`{ when = … }`), which is otherwise untouched: with a raw `date` the old concatenation would have printed an ISO date under a panel set to anything else. Gestures and `README.md` are untouched. `bar.luau` exports `elapsed` now. `getfenv(bar.Config.get)` was how the suite reached bar's globals table, and the lift took that away — `Config` belongs to the module now. `elapsed` is the one function `bar.luau` still defines itself. The suite's host trap (`test.luau:2107-2124`) changed subject: it now traps `Viewmodel.build` rather than `panel.viewModel`, because `viewModel` calls `formatOpts` and 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 `formatOpts` does, which is what stops the first from passing on a panel that formats nothing. ## Scratch teardown ``` $ noctalia msg plugins list | grep -c u6probe 0 $ noctalia msg plugins list | grep -c u6live 0 $ grep -cE 'u6live|u6probe' ~/.local/state/noctalia/settings.toml 0 $ noctalia msg plugins source list official git https://github.com/noctalia-dev/official-plugins community git https://github.com/noctalia-dev/community-plugins my-dev path /home/yuris/Development/other/noctalia-timew $ ls -d /tmp/u6* ls: cannot access '/tmp/u6*': No such file or directory $ noctalia msg plugins list | grep timew yuris/timew [my-dev] 1.1.0 enabled requires timewarrior ``` Log level was set back to `info`. The enabled `yuris/timew` was never touched and `settings.toml` was never hand-edited. ## What a reviewer should challenge - **The narrowing to one bit of `timeFormat()`.** A user whose format is `{:%Hh%M}` gets `17:48`, not `17h48`. Defensible on the epoch measurement and the column width, but it is a judgement call and the alternative is a per-row `formatTime` on an epoch reconstructed from the UTC offset. - **Marker translation.** `Fmt.hhmm` takes `{ am, pm }` from the caller because it is pure. That means `panel.am` / `panel.pm` are this plugin's strings, not the locale's `%p` — a Spanish user with an English plugin locale gets `AM`, where `formatTime` would have given them the C locale's answer. - **`iso` as `08-31`.** Month-day, no year, hyphenated. It is ISO field order, not an ISO date, and the label says `ISO — 08-31` to admit that. A reviewer may want it named `ymd` or dropped. - **`[[panel.setting]]` versus plugin-level.** Scoped to the panel because the bar shows no 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: anything reading `row.date` expecting `dd/mm` is now wrong. The suite covers the three readers in 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.
yuris left a comment

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.time finding is real and subtle. Reproduced on this machine:

os.date("%H:%M", 1756700000)                        -> 11:13   (local)
os.time({year=2025,month=9,day=1,hour=11,min=13})   -> 1756725200
delta                                                -> 25200  (= +07:00)

Luau's os.time reads the table as UTC while os.date renders local, so timew's local
wall-clock text cannot be turned into an honest epoch in pure Luau. That correctly rules out
per-row formatTime with a user pattern, and it is the kind of thing that would have shipped
as a seven-hour bug in one timezone.

Three rejections, all better than accepting. 12h/24h is already answered shell-wide by
timeFormat(), and Config.hour12 reading %I %l %r %p %P out of the user's own pattern is
the 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/09 reads as 9 January to a US reader. Options are the keys of Fmt.DATE_ORDERS and
CONFIG_OPTIONS is that same table, so accepted and implemented cannot drift.

Gates: 12 states, 1739 field comparisons / ok; 0 errors, 0 warnings. Probe torn down
clean: 0 in plugins list, 0 in settings.toml. config.luau has its .luau.luau alias.

Mutations

# Mutation Result
M1 default date order renamed killed
M3 mdy aliases dmy killed

Plus 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, '', mdy and iso seeded as the manifest default - is
better evidence than the unit asserts alone. Worth keeping that method for the next setting.

**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.time` finding is real and subtle.** Reproduced on this machine: ``` os.date("%H:%M", 1756700000) -> 11:13 (local) os.time({year=2025,month=9,day=1,hour=11,min=13}) -> 1756725200 delta -> 25200 (= +07:00) ``` Luau's `os.time` reads the table as UTC while `os.date` renders local, so timew's local wall-clock text cannot be turned into an honest epoch in pure Luau. That correctly rules out per-row `formatTime` with a user pattern, and it is the kind of thing that would have shipped as a seven-hour bug in one timezone. **Three rejections, all better than accepting.** 12h/24h is already answered shell-wide by `timeFormat()`, and `Config.hour12` reading `%I %l %r %p %P` out of the user's own pattern is the 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/09` reads as 9 January to a US reader. Options are the keys of `Fmt.DATE_ORDERS` and `CONFIG_OPTIONS` is that same table, so accepted and implemented cannot drift. **Gates:** `12 states, 1739 field comparisons / ok`; `0 errors, 0 warnings`. Probe torn down clean: 0 in `plugins list`, 0 in `settings.toml`. `config.luau` has its `.luau.luau` alias. ## Mutations | # | Mutation | Result | |---|----------|--------| | M1 | default date order renamed | killed | | M3 | `mdy` aliases `dmy` | killed | Plus 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`, `''`, `mdy` and `iso` seeded as the manifest default - is better evidence than the unit asserts alone. Worth keeping that method for the next setting.
yuris merged commit 2aaa98370b into main 2026-09-01 20:21:28 +00:00
yuris deleted branch upgrade/u6-format 2026-09-01 20:21:29 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/noctalia-timew!17
No description provided.