refactor(panel): swap the call sites onto the pure core, key the last literals #7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refactor/t4-swap"
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?
Wave 3, second and final PR. Two passes on the same call sites: point every
caller at the pure core that landed in wave 1 and delete the duplicates, then
move the last user-visible literals into translation keys.
Rendered output is unchanged. Proof below.
Part 1 — call sites onto the pure core
panel.luautagColor(7 lines)Query.tagColor(tag, TAG_COLORS)trim(3 lines)Parse.trimstartTag,setFilterformatElapsed(7 lines)Fmt.clockrenderformatSpan(7 lines)Fmt.spanrenderelapsed(6 lines)dayTitleexpressionFmt.dayLabel(entry, index)barFill/letterColorQuery.barRoles(mode, filter, seconds, isSelected)activityRowsQuery.windowmath.ceil(matching / ROWS_MAX)Query.pageCountgotoPage,rendermath.floor(listOffset / ROWS_MAX)Query.pageOfonPagePrev,onPageNext,renderfirstShown/lastShownQuery.pageStripThe last four rows are past the brief's list. They were the reason to look:
Query.window,pageCount,pageOfandpageStriphad no production callerat all after wave 1 — four tested functions the panel was not using while it
carried its own copy of each. Leaving them would have been shipping the pure
core as dead code.
Grep, after each deletion
Nothing.
Parse.trimand the other qualified names are excluded by theleading-
.guard, so this is specifically "a bare call to a helper that nolonger exists". Also checked:
grep -c 'getfenv\|setfenv' timew/panel.luau→0.shQuote— one definition, and whichshQuoteis not duplicated by the pure core;Parsehas no quoting function,so there was nothing to delete. It stays a module local rather than moving into
Parse, for two reasons: it is not text → data, andTimew.tagArgalreadyexports the whole injection surface, which is where
test.luau's seven quotingasserts point. It has three callers —
Timew.tagArg,Timew.cmd.intervalsForDay,and
startTag, which buildstimew start <tag>itself becausebar.luau:215builds the identical string with its own copy and the plugin API forbids
requirebetween entries. That is the same reasoning PR #6 recorded onTimew.exec, unchanged.elapsed— the third answerNeither "stays" nor "moves to the pure core". It is gone, inlined at its only
call site:
elapsed()wasif not active then return 0 end; return os.time() - startEpoch.Its guard is dead here: the
not activebranch above has already answered, sothe function reduced to one subtraction. The pure-core version would have been
Query.elapsed(active, startEpoch, now)— three threaded arguments to expressos.time() - startEpoch, and a function with nothing a test could catch itgetting wrong.
Fmt.clockstill clamps negatives, so a start epoch in thefuture reads
0:00exactly as before. The comment onupdate()that promised"elapsed() is recomputed from the start epoch" moved to the call site rather
than being left pointing at a function that no longer exists.
Part 2 — literals → keys
"--"inFmt.dayLabelpanel.no_day--"--:--"(idle clock)panel.clock_idle--:--"(untagged)"shown on a row / the subject linepanel.untagged(untagged)filter .. " ✕"panel.filter_chip{tag} ✕`{listOffset+1}-{listOffset+#shown} / {matching}`panel.page_range{first}-{last} / {total}Fmt.dayLabelnow returns("panel.no_day", true)for a nil entry instead of("--", false), so all three of its answers go through the sameisKeyrouteand the caller has no special case. Its unit assert moved with it.
After the swap the only string literals left in a
text/placeholder/tooltipprop are the two chevrons:The
(untagged)trap, and how it is handledOne string was doing two jobs. It is what a row displays, and it is also:
~=infilterFromRow, to refuse a filter nobody can act on;Timew.tagMatchesas a row tag when the search runs;Query.tagColorfor the row's colour dot;bar.luau:180into thetimew.tagsshell state, whichadoptStatereads straight back into the panel.That last one settles it. It is a wire value between two plugin entries that
cannot
requireeach other, so it has to be the same bytes in every locale.Translating it breaks the filter, the search and the dot's colour stability, and
does so silently — the panel would look right in English and stop refusing the
untagged row in Spanish.
So the two jobs are split. The sentinel is a module constant,
UNTAGGED, and itis what
Parse.interval,Timew.parseActiveandfilterFromRowuse. Displaygoes through one function:
with three call sites: the row button's text, that button's
panel.filter_bytooltip, and the tracking subject line.
panel.untaggedresolves to(untagged)inen.json, so English is byte-identical; a translator changingit changes only what is on screen.
Chevrons and ✕
‹and›stay literals. They are the button's shape, not its text: a key herehas exactly one legitimate use (an RTL locale flipping them) against one likely
misuse (a translator writing a word into a five-button strip pinned to
RIGHT_W, pushing the page numbers off the panel). The prose naming the actionis already a key, on the tooltip of the same button —
panel.newerandpanel.older. The rule applied: a standalone glyph is a shape and stays; astring that composes chrome with user data is a template and gets a key.
Which is why the ✕ went the other way. The glyph needs no translating, but the
chip is
tag+ spacing + glyph, and that ordering and spacing are prose — anRTL locale wants the ✕ on the other side.
panel.filter_chipis{tag} ✕, sothe composition is translatable and the glyph inside it is not something a
translator has to retype.
Not translated, on purpose
"0:00"and"0:00:00"— the zero defaults inTimew.parseTotal,Timew.parseRangeTotaland the day total. They are values in timew's own clockformat, sitting in the same label as real durations from
Fmt.span. A localethat "translated" one would put a non-clock string where the panel reads h:mm.
Parity evidence
Throwaway harness (not committed,
/tmp/parity/harness.luau): requiresgit show main:timew/panel.luauand this branch's copy as two modules, stubsone host into each module's own globals table, pins
os.time()so the trackingclock is deterministic, and resolves
noctalia.tragainst the realen.jsonso a key's resolved English text is compared, not the key. It then runs one
identical script against both and diffs the flattened UI trees — one line per
node,
kindplus every prop sorted by name, under an index path, so label text,colour role, handler name, tooltip and every dimension are all in the
comparison.
The 30 states: open with nothing landed; recent chips; today's total; the 14
day bars; today's intervals (which include an untagged row and a multi-word
tag); tracking a plain tag; tracking a multi-word tag; the refetch after a
start; tracking with no tag; back to idle; yesterday selected (named-day label);
a dated day with no intervals; back to today; clicking the untagged row;
filtered day scope; filtered all-time one page; all-time with 37 matches at
page 1 of 5; pages 2, 3 (middle), 5 (last, short), 4, 1; all-time unfiltered
with the untagged row in the all-time lead column; back to day scope; a filter
nothing matches; cleared; a case-insensitive search submit; a draft in the tag
field on page 2; a recent chip starting its tag 60 s in; a second tick.
The harness was checked against itself before the result was trusted. Two
deliberate mutations of the branch copy:
TAG_COLORS[1]primary→error— 11 differences,PARITY FAILEDpanel.clock_idle→panel.no_dayon the idle clock, i.e. resolved English--:--→--— 24 differences,PARITY FAILEDBoth restored byte-identically (
cmpclean), back to 0.Tests
test.luaugoes 615 → 686 lines. Updated: theformatSpanandformatElapsedloops now run the surviving definitions only, the bar/panel drift section
compares bar's copies against the single definition panel is left with,
getfenv(panel.formatElapsed)becamegetfenv(panel.Fmt.clock), andFmt.dayLabel(nil)is pinned to the key. The export table lostformatElapsedandformatSpan.Thirteen new asserts, each proved live by mutating
timew/panel.luauandchecking the suite dies on that exact line:
Two assert pairs were reordered so neither hides the other under mutation: the
(untagged)-never-on-screen check runs before the row-text check, and the"the built string is gone" check runs before "and the key is there".
The sentinel asserts are the ones to keep. The stubbed
trhands back the key,which is exactly how a key is told from a literal on the rendered tree: a row
still reading
(untagged)down there never went throughtrand would stayEnglish in every locale. And
onIpc("row", 1)on the untagged row must leavethe trend caption unfiltered, with the real-tag row below it as the control —
without that control, a dead click path would pass just as well.
Gate
Note on gate 2:
noctalia plugins lintcross-checks declared settings againstgetConfig()calls and nothing else — it does not parse Luau and does notvalidate translation keys. I confirmed that by appending a syntax error and
watching it still report
0 errors. The Luau parse is covered by gate 1, whichrequirespanel.luau.Not done, on purpose:
renderis not split into region builders, there is noviewModel,bar.luauis untouched, and no layout, spacing, colour orgeometry changed.
What to screenshot after merge
The parity harness covers the tree, not the pixels. These are the states where
a rendered difference could still hide:
--:--and the left scope button reads
--. Both are keys now; a missing key wouldrender as the key name.
Today, the total to itsright, bars unfiltered.
tag as plain text at
SUBJECT_H, the card does not resize when it starts.(untagged), its dot has the same colour it had before this PR, and itsduration is right-aligned with the header total.
Then click the row below it — the chip appears and reads
<tag> ✕with thesame two spaces before the glyph.
Clear tag filtertooltip, click the ✕,confirm the chip clears and the list re-lists.
primary/0.5, the selectedbar at full
primary, day letters under the lit bars inprimary. This isthe
Query.barRolesswap; it is the one place a colour role could have moved.1, a middle page, and the last page: the
‹/›ends, the current pagetinted, and the range label at the right reading
9-16 / 37. Confirm thestrip slides rather than growing, and that the range label does not wrap.
Nothing trackedunder a dated scope buttonlike
Sat 29/08.same second.
elapsed()was inlined; this is where that would show.Verdict: APPROVE
Reviewed as a swap-and-key pass, not a re-derivation of wave 1. I spent the
budget on the three things this kind of pass gets wrong — a call site that now
calls something subtly different, a deleted helper with a live caller, a key
that breaks a comparison or a layout — and proved the first one by rendering
both trees rather than by reading.
Headline: rendered output is unchanged, and I mean byte-identical. I loaded
main'spanel.luauand this branch's side by side in one luau state, gaveboth the same stubs and a
trbacked by the realen.jsonvalues, drove 13states through the IPC surface, serialised each rendered tree with sorted props
and diffed:
The 13 cover every swapped call site: fresh open (
panel.no_dayvs"--"),day index 1 (key branch) and index 3 (verbatim
Sat 29/08), a day with anuntagged row (
tagLabel+Query.tagColor+Query.windowwith a predicate),a day with nothing tracked, all-time + chip (
Query.windowwithnil), andthe pager at page 1 / middle / last on both a 3-page and a 5-page list so the
strip has to clamp left, centre, and clamp right. That harness stubs an active
interval throughout, so a second run with
timew.activefalse covers the branchit misses:
So
Fmt.clock(os.time() - startEpoch)andformatElapsed(elapsed())producethe same label,
Query.pageStripslides the same wayfirstShown/lastShowndid, and
Query.windowreturns the sameshownslice and the samematchingcount as the loop. Three findings, none of them in that arithmetic.
Findings
1 — P2. The untagged row's tooltip and the search box no longer agree with what the row says
timew/panel.luau:1028-1036The sentinel split is right where you aimed it:
filterFromRow(:799) comparesUNTAGGED,Query.tagColor(:1022) hashesUNTAGGED, and my locale probeconfirms the row dot keeps the same fill in a Spanish locale. The gap is the
other consumer of that string.
passesFilter(:908) matches the needle againstrow.tag, which is still(untagged)— but the row now readstr("panel.untagged"). So the string on screen is no longer the string thatmatches:
A user who reads the row and types it into the search box gets nothing; only
the English literal, which appears nowhere on their screen, works. Before this
PR the row read
(untagged)in every locale and searching it worked. Thetooltip you touched on :1036 makes it worse by promising the action in the
user's own language — on a row whose click
filterFromRowdeliberatelyrefuses.
Latent today: only
en.jsonships, sotr("panel.untagged")returns thesentinel and nothing is reachable. That is why this is not a block.
Making the search agree would mean matching the label in
passesFilterandin
Timew.parseIntervals, since all-time filters upstream — two call sites anda
trin a per-row loop, for a row you have already decided is not a filtertarget. The cheaper fix is to stop advertising the action, which also drops a
tagLabelcall:An absent
tooltipprop is already how the page-number buttons at :1245 arebuilt, so this needs no new host behaviour.
2 — P3.
--:--and--became keys under the rule that kept‹/›literaltimew/panel.luau:1069-1072,timew/translations/en.jsonThe comment you added at :1227 states the rule and states it well: a glyph
placeholder stays a literal because "a word here pushes the number strip off a
row pinned to
RIGHT_W".panel.clock_idlesits in a tighter box than thepager does. Its label is
fontSize = 28, fontWeight = "bold"insideui.column({ gap = 4, width = LEFT_W - ACTION_W - 24 })— 120 px (:1100) — andthe panel cannot clip.
--:--is five narrow glyphs; a word is not.en.jsonalready carries an unused
panel.idle: "Not tracking", which is exactly theprose a translator handed
panel.clock_idlewould reach for.panel.no_dayhas the same shape on the scope button in the
width = RIGHT_Wheader.Both are placeholders for a value, not prose, by the test the same patch
applies to
‹/›. Either keep them literal or record the ceiling where atranslator will see it — a
"_comment"key beside them inen.jsonnaming the120 px and the 28 pt costs nothing and is the only place the constraint could
land, since translators never read
panel.luau.The ruling you asked for on
‹/›: correct as literals, and for the reasongiven. They are direction, not language; RTL is the one case that would want
them mirrored, and the shell has no RTL story at this API to mirror them into.
3 — P3.
bar.luau:156is now the only screen the sentinel still reachestimew/bar.luau:154-157setTooltip(tags)renders the value :180 set to"(untagged)", verbatim.bar.luaucontains zeronoctalia.trcalls, so it has no route topanel.untaggedand this PR correctly left it alone. But the divergence isnew: before, both surfaces said
(untagged); now the panel translates and thebar does not, and your own screenshot item 10 puts them side by side. Worth an
issue rather than a change here — translating the bar means giving
bar.luauits first key, which is a separate decision from this one.
Verified
Gate, run here, not quoted from the description:
Three files, no excluded file, no
getfenvin either plugin entry (test.luaukeeps two, which is where it is allowed).
The
panel.untaggedlocale experiment. I ran the one you would expectfirst: set
panel.untaggedtoZZ_LOCALE_PROBE_ZZinen.json, ran the gate.Both stages still pass — because
test.luaunever readsen.json(noreference to it anywhere in the file; the harness stubs
trto hand back thekey). So the suite alone proves the split is wired, not that it is
locale-independent. Restored:
cmpbyte-identical, md5fbfd4772…before andafter,
git status --porcelainempty.The experiment that does prove it runs
panel.luautwice with two realtranslation tables, English and a Spanish one that also flips the chip glyph to
the other side:
Clicking the untagged row sets no filter in either locale; clicking the real
tag sets one in both; the row dot's colour role does not move; the sentinel
never appears on the Spanish tree and the Spanish string never appears on the
English one. A locale changes what the row says and nothing about which rows
are filterable — which is finding 1's mirror image: it is too independent in
the search box.
Deleted helpers. Reproduced your grep and widened it to catch a survivor in
a comment or a string:
Every survivor is either prose or the qualified
Query.tagColor. The exporttable returns
Fmt,Parse,Query,Timew, all four defined.test.luaureaches for
panel.Fmt/Parse/Query/Timewandbar.Config/formatElapsedonly —the single
panel.formatElapsedleft in it is inside the comment at :33, not acall.
Keys. Every
noctalia.tr("…")intimew/*.luauresolves againsten.json: 22 used, 0 missing.panel.no_day/today_short/yesterdayreach itthrough
Fmt.dayLabel's return rather than a literal call site, so they arelive too. Byte-level check on the four moved literals, since a lost space is
the classic key regression:
The chip gap and the glyph survive substitution unchanged, and the tree diff
above confirms it end to end. Numeric substitution into
page_rangeis not newground —
panel.trendhas passed{ days = DAYS }since before this PR.Header overflow.
panel.filter_chipcosts nothing in English: the chip isthe same string it was, and the header row still contains two scope buttons, the
chip, a
flexGrowspacer and aDUR_Wtotal insidewidth = RIGHT_W. Thevariable part is the user's tag, which was already unbounded before this PR, so
the key adds no new overflow the panel did not already have.
Asserts. No pre-existing assert weakened. The two loops that compared a
deleted duplicate against its replacement lost the deleted operand and kept
every value and every message:
Fmt.spanstill runs the same nine, theformatElapsed/Fmt.clockdrift loop still runs the same fourteen includingthe fractional and negative ones. The one changed assert
(
Fmt.dayLabel(nil, 1)) tracks the intended behaviour change. Net five newblocks.
listOffsetpast the match count. You asked, so:Query.windowreturns anempty
shownand a truematching,currentlands abovepages, no pagebutton reads selected,
‹stays enabled and›disabled, and the range labelreads backwards (
17-16 / 10). That is reachable —fetchDays→loadDay(sel, true)does not rewind the pager and the day can shrink under it — but it isbyte-identical to
main's arithmetic, so it is not this PR's. Flagging it onlyso it is written down somewhere.
Answers
elapseddeleted rather than moved — agreed, and it is the best of the threecalls in this PR. The guard was dead: the only call site sat in the
elseofif not active, soelapsed()could only ever returnos.time() - startEpoch.Moving a dead branch into the pure core would have meant testing it, and a test
for an unreachable branch is worse than no test.
Fmt.clockclamps negativeswith
math.max(0, …), which is the only thing the guard was protecting againstin practice, and my idle-state parity run exercises both sides of the branch it
used to sit in. The
startEpoch = 0whileactivecase behaves identicallybefore and after, so nothing regressed there either.
shQuotestays a module local — agreed, and the reasoning holds up undergrep. Three callers, exactly as claimed:
Timew.tagArg(:398),Timew.cmd.intervalsForDay(:447),startTag(:728). It is not text → data,so
Parseis the wrong table for it; and sinceTimew.tagArgis the exportedinjection surface, moving
shQuoteintoParsewould givetest.luaua secondplace to point quoting asserts at without giving it anything new to assert. The
startTagthird caller is the sameno require between entriesconsequenceTimew.execalready documents, and the comment says so instead of re-arguingit.
Scope beyond the brief — right call, and the diff is the argument. Four
tested functions with zero production callers while the panel carried its own
copy of each is not "extra scope", it is the wave being finished. Shipping the
pure core as dead code would have left the next reader to discover the panel
disagreed with its own core. And the extra scope is subtraction:
panel.luauis 88 lines shorter for it, with the growth in comments rather than code. The
one thing I would push back on is the stated reason for
passesFilter(:907)being a module-level function — one closure per second is not a 25 ms budget
problem. The code is right anyway; a named predicate beside the call reads
better than an inline one, which is reason enough on its own.
Panel states worth screenshotting after merge
Your list is the right list; the tree diff removes the need for most of it.
These are the ones a tree cannot answer, in the order I would shoot them:
show the same second, and the card must not resize when tracking starts.
elapsed()was inlined andSUBJECT_His doing the work; both are geometry.against a pre-merge shot. The tree proves the role name; only pixels prove
the theme resolves it the same way.
primary/0.5, selectedat full
primary, day letters under lit bars inprimary. The one placeQuery.barRolescould have moved a role.and the range label must not wrap off
RIGHT_W.<tag> ✕with both spaces, hover forClear tag filter,click to clear.
--:--, scope button--. If eitherrenders as its key name the merge is missing an
en.json.Sat 29/08plusNothing tracked, the two states finding 2 is about.The untagged row's tooltip offered "Filter by (untagged)" through tr, so in a translated locale it promised the action in the user's own language on the one row filterFromRow deliberately refuses. Search cannot reach it either: passesFilter matches the needle against the UNTAGGED sentinel, not against the translated label the row displays. Drop the tooltip on that row rather than teach passesFilter and Timew.parseIntervals to match a per-row tr call for a row that is not a filter target. An absent tooltip prop is how the page-number buttons are already built. Also revert panel.clock_idle and panel.no_day to literals, applying the rule the same pass wrote for the pager chevrons: a placeholder is the shape of the value, not text about it, so a key buys nothing a translator could get right and costs the one thing they could get wrong. The idle clock is 28 pt bold in a 120 px column and the panel cannot clip; en.json's unused panel.idle ("Not tracking") is exactly the prose a key there would invite. Asserts pin all three: the untagged row is displayed translated, is never a filter target, and carries no tooltip while the row beside it does.All three findings handled in
7cf16da. Thanks for rendering both trees ratherthan reading them — the parity dump is what made finding 1 a one-line fix
instead of an argument.
1 — P2, applied as suggested
tooltip = if row.tag == UNTAGGED then nil else noctalia.tr("panel.filter_by", { tag = row.tag }).Your analysis of the alternative is right and I did not attempt it: teaching
passesFilterandTimew.parseIntervalsto match the translated label means atrcall per row in a per-second loop, to make searchable a row whose clickfilterFromRowrefuses on purpose. The row is not a filter target by any route,so it should not advertise one. The
elsebranch drops back torow.tag— thesentinel is the only tag
tagLabelrewrites, so the surviving branch neverneeded it.
Re-ran your comparison against the untagged state, base = this PR as you
reviewed it, head = the tooltip fix alone, same stubs, props sorted:
One node moves, the prop leaves rather than emptying, and the real tag's row
beside it keeps its tooltip.
2 — P3, resolved by reverting both to literals
I took the first of your two options, because the rule I wrote at
:1227already covers them and I would rather have one rule than one rule plus a
footnote:
--:--and--are the shape of the value, not text about it, by exactly thattest. The
_commentroute records the ceiling in the one place a translatormight look, but it only documents the failure — a translation memory or an
MT pass never reads it, and
panel.idle: "Not tracking"sitting three linesabove
panel.clock_idleis precisely the substitution it would make. A literalremoves the failure instead. The clock is the acute case at
fontSize = 28bold in a 120 px column;
panel.no_dayshares theRIGHT_Wheader row with thesecond scope button and the chip. Neither can clip, and neither gains anything
from a locale.
Fmt.dayLabel(nil, …)returns"--", falseagain, both keys are out ofen.json, and the comments at both sites and intest.luaunow cite the pagerrule by name so the next reader finds one rule applied three times. I left the
unused
panel.idlealone: it predates this PR and translation keys are publicsurface.
3 — P3, recorded, not changed
Agreed it is out of scope: translating
bar.luau:156means giving that entryits first
noctalia.trcall, which is a decision about the bar, not about thisswap. Recorded in
REFACTOR.mdunder## Deferred, together with thelistOffset-past-matchingcase from your Verified section — that one isbyte-identical to
main's arithmetic, so it is older than this wave, but itdeserved to be written down somewhere and now it is.
Asserts
The untagged row's contract is pinned in all three parts, read off the tree
before the clicks that change the list:
clickedTextsis nowclickedProp(…, "text"); the walk keeps itstexttypeguard whichever prop it reads, so both lists index the same nodes, and
tostringkeeps an absent prop in the list as"nil"rather than dropping thenode and sliding the rest left. The control row is what stops it passing if
every tooltip disappears. Two more assert pairs cover finding 2: the scope
button shows
--and notpanel.no_day, the idle clock shows--:--and notpanel.clock_idle.Liveness by mutation, each reverted byte-identically (
cmpclean,panel.luaumd5
7cd2c2df…before and after):Full-head parity on the same untagged state now shows three differing nodes of
40 — the tooltip plus the two reverted literals — and nothing else:
Gate
Keys: 20 literal
noctalia.tr("…")call sites intimew/*.luau, 0 missing fromen.json; the only non-literaltrargument left isFmt.dayLabel's return,which the
isKeyguard now keeps--out of.