| Age | Commit message (Collapse) | Author | Files | Lines |
|
|
|
|
|
disks draws a ring per filesystem from the df sample, with the NFS
server address read from the device field and a labelled-bar fallback
when the rings would be too small to read. cache shows the ~/.cache
total and its four largest children from the du sample.
card.ring now resets the cairo path first: a preceding card.text()
leaves a current point, and cairo_arc() joins to it with a straight
line, so the disks rings were drawn with stray chords. The ring
primitive had no caller until now, which is why it went unnoticed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Eleven tasks. The palette's warning role first, since everything else
colours against it; then an offscreen render harness, so each later task
can look at its work without an install-restart-toggle cycle; then
parsers, samplers, primitives, the four cards, and the live pass.
Glyphs are taken from the old conky config and re-rendered rather than
guessed. That matters: a guess at the Slackware mark drew a stomach, and
the config had the real codepoint (U+F318) all along, along with the
disk and network-share icons. The icon is the label now, so no mount
name is drawn.
The two NFS shares are told apart by their server's address, read from
the mount's device field at runtime. The repo is public, so the address
belongs on screen and not in a committed file; deriving it also means
the card survives a renumbered LAN. df_parse keeps the device field for
this, and `host` is non-nil exactly for a network mount.
Two traps recorded because both would fail silently: df is aliased to
df -h in the user's shell, so the sampler calls /usr/bin/df -P -B1 by
absolute path or the parser reads '1.6G' where it expects bytes; and the
per-core counters must outlive a frame, since load is a delta and a
counter rebuilt each draw reports nil forever.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Thresholds colour every quantity that has a comfortable range: ok below
the first, warning between, critical above. 25/75 for filesystems, cache
and CPU load; per-sensor pairs for temperatures, because 70C is
unremarkable on a CPU and alarming on an NVMe.
The per-core row becomes an equaliser: sixteen vertical bars rising from
a baseline, coloured by load. Disks become idea2's rings, an icon at the
centre and the arc coloured by usage. Cache highlights its largest
subdirectory.
This needs a `warning` role the dashboard does not currently expose.
udt-palette already resolves it in every scheme, mapping yellow (or
Nord's aurora_yellow) to it globally, and other generators already read
it, so adding it is one word in gen_conky's role tuple plus a
placeholder and a palette line. No scheme file changes.
Three new primitives in lib/card.lua rather than per-widget drawing:
bar, ring, and a threshold helper so all four cards agree on the rule
instead of each re-deriving it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Four cards: CPU/RAM with every temperature, GPU, filesystems, and the
user cache. Three were asked for; cache became its own card because it
is a different refresh cycle and sharing the disks card would have made
the densest card in the dashboard.
Two findings that shaped it, both from probing the hardware rather than
assuming:
- The Arc B580 exposes no utilisation figure. The xe driver has no
gpu_busy_percent, intel_gpu_top refuses the device outright, and
gputop prints per-process rows with ANSI escapes, which is not an
interface to build on. The card shows temperatures and fan speed and
no load bar, rather than inventing a number.
- /data is on a spinning disk with no hwmon chip, so there is no sda
temperature to show. Reading SMART would need smartctl as root and
would spin up a sleeping disk each poll.
Also: the four NFS mounts are two filesystems behind two servers, so
showing all four printed every number twice. One mount per server.
Both new samplers follow the weather fetch's cache-and-read pattern,
which exists precisely so an unreachable NFS server costs a stale cache
instead of freezing the draw hook.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A finer grid to place cards in smaller increments. The clock takes a
fractional 1.5-column span, which rect_for supports because it is plain
arithmetic with no integer assumption; note that a span of w covers w
cells plus the w-1 gaps between them, so 1.5 is slightly wider than one
and a half bare cells.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The numeral size was capped at a flat 0.46 of the available height,
which for two baselines spanning 1.70x the size meant they could only
ever fill about four fifths of the band, leaving a void above the date.
The divisor is now derived from the baselines themselves.
The width cap is measured rather than guessed: two Oswald digits are
1.00x the font size wide (at size 100, '00' measures 100.0px and the
widest pair '88' 98.0px), so a width w permits a size of about w, less
a margin at the card edges.
A narrow cell is still width-bound and cannot fill its height. That
slack is now split above and below the numerals instead of collecting
under them, so the pair sits centred in the space it cannot fill.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Horizontally centred in the card; the weekday and date stay against the
left edge at the bottom.
Each line is centred on its own ink width via measure(), not advance():
ink is what centring wants, and Oswald is proportional, so 10 and 05 are
not the same width and a shared measurement would leave one of them
visibly off-centre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Installs Oswald (OFL-1.1, from googlefonts/OswaldFont) under
~/.fonts/o/Oswald with its license, and points the clock numerals at it
for the squared look the rounded Noto Sans Black did not have.
Per-widget fonts needed no new machinery: card.font() already takes any
family string, so FONT_* are shared defaults rather than a restriction.
FONT_CLOCK joins them, and the weather card keeps FONT_HEAVY, so the two
no longer have to agree.
Cairo still cannot ask for a weight above bold, so a heavier cut is
selected by family name as FONT_HEAVY already did. Worth knowing that
fontconfig substitutes a default for a family it does not recognise,
which is indistinguishable from the font not applying; fc-match tells
you which happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Temperature and city stay against the left edge; the glyph moves to the
right edge, so the header reads as a value on one side and its icon on
the other rather than one run of text.
text_right takes the right edge as its x and measures ink, which is what
aligning to an edge wants, as against advance() for stepping a cursor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The conky surface is a fixed 2560x1080, because Hyprland's fullscreen
rule resizes the window but not the drawing surface, so on the
1920-wide DP-3 the dashboard would be cropped. A workspace rule keeps it
on the primary monitor.
hl.workspace_rule takes `workspace`, not `match`, and has no `name`
field, unlike hl.window_rule. Both were checked against the runtime,
which rejects an unknown field with "unknown field '<name>'", and
hyprctl workspacerules confirms the rule registers with monitor: DP-1.
NOT fully verified: whether it holds when the toggle is pressed while
DP-3 has focus. One observation suggested the dashboard still followed
focus, but it was taken before the reload had applied the rule, and
focus could not afterwards be moved to DP-3 without a window or the
pointer on it. Every toggle since the reload has opened on DP-1. Worth
confirming after a fresh Hyprland start.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Editing the layout table or a widget needs a conky restart and nothing
else, since the Lua is symlinked and conky never rereads its config.
That was three commands to remember, one of them the hyprctl dispatch
form that keeps the dashboard from popping open over the screen.
The script syntax-checks before killing anything and refuses to restart
on a Lua error, naming the file and line. Without that the first sign of
a typo is a blank workspace with no message on any stream, which is the
single most confusing failure mode this project has.
Falls back to a plain exec where Hyprland is not running, and says what
to run by hand if conky fails to come up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The numerals scaled off the cell but the date block sat at a fixed drop
below them, so a tall cell left a dead band under the date and a short
one would have collided. Now the date pins to the bottom edge, the
numerals take everything above it, and both type sizes derive from the
width, so the slack of a tall cell falls in the middle of the card where
it belongs.
Type scales off width for the same reason the weather card does: height
is what changes when a row is added to the grid, and a widget that
shrinks its text whenever its cell gets shorter reads as broken next to
its neighbours.
Checked at 1x3, 1x6, 2x2, 2x4 and 3x3 on an 8x6 grid before restarting
conky. The 1x6 case stays width-bound and leaves room below the
numerals, which is correct for a cell far taller than it is wide.
The README now states the convention, since it applies to every widget
added later: derive offsets from the rect, scale off width, decide where
the slack goes, and drop an element that would otherwise overlap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The grid was 4x2, which made every placement decision a coarse one. 8x6
gives 299x157 cells, so a card can be sized in something closer to the
increments the layout actually wants. Nothing in rect_for needed
changing: it derives cell size from COLS and ROWS, so this is a
two-number edit.
Note that ROWS has to cover the tallest row in use. A widget placed on
row 3 of a two-row grid is computed at y=1068 on a 1080px screen and
draws off the bottom, silently.
The arc band no longer has a minimum height. It had one, and in a cell
40px shorter than the content wanted, the minimum won and pulled the
curve up through the stat rows, drawing the sunrise time across the word
WIND. It now takes what is genuinely left and is skipped below the
height where it could be read, so the same widget composes in a 2x2 or a
2x3 cell without overlapping anything. Checked by rendering the widget
offscreen at 2x1, 2x2 and 2x3 before restarting conky.
Layout is now a 1x3 clock with a 2x3 weather card beneath it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Adds a Weather section covering the API key, where it lives and why, the
401-while-activating behaviour a new key shows, the execi refresh, and
the by-hand fetch check. Adds five gotchas: execi firing under empty
text, the atomic cache write, what OWM actually returns for a bad key
and city, the no-raise invariant in lib/weather.lua, and type scaling
off width.
Also corrects three things that had gone stale: the symlink list omitted
bin/, the layout example still showed a single full-height clock, and
the test line omitted test_weather.lua.
The screenshot recipe was wrong in two ways, both of which bit during
this work. It slept a fixed second and raced the compositor, which is
how a capture came back showing a terminal instead of the dashboard; it
now polls for the workspace switch. And it fed the window's geometry to
grim -g, but a window on a hidden special workspace reports a negative
Y, so that grabs the wrong region; it now captures the output. Every
command in the file was run as written before committing this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Placement is column 1 row 2, directly beneath the clock, which drops to
a single-row cell to make room.
The card's type now scales off the cell WIDTH rather than its height.
Scaling off height meant the text shrank every time the cell got
shorter, and beside the clock's numerals the card read as a whisper.
Width is the stable dimension for this layout and is what constrains the
text anyway, since the stat rows run edge to edge.
Verified against the live dashboard, not only the offscreen render: the
two disagreed about type size precisely because the offscreen harness
was cropped to the card and never showed it next to the clock.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The first draft placed every band at a fixed pixel offset and let the
arc absorb whatever height was left. In a 1x2 cell on this monitor that
is 794px of arc: a cathedral arch under a fifth of a card of content,
nothing like the mockup it was shaped after.
Three changes, all visible in an offscreen render of the real widget:
- Type sizes and band offsets scale with the rect, within clamps, so the
proportions hold on the 1920 monitor as well as this one.
- The arc gets a bounded band that follows the stats instead of being
pinned to the bottom, so no dead strip opens between them.
- The layout gives the widget a 1x1 cell. Its content is about 350px
tall and a full-height cell cannot be filled without inventing data.
Found before restarting conky by loading conky's own cairo bindings
standalone and rendering the widget to a PNG, which is a far tighter
loop than install, restart, switch workspace, screenshot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Draws current conditions plus a sunrise-to-sunset arc in widgets/weather.lua,
placed at col 2 in dashboard.lua's layout. Schedules the periodic fetch via
execi in conky.conf.in, tying refresh to conky's own lifecycle.
The no-data env-file probe now closes its handle explicitly: the branch reruns
every draw until a fetch succeeds, so an unclosed io.open there would leak one
handle per frame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
`set -a; . "$ENV_FILE"; set +a` on one line meant the
`# shellcheck source=/dev/null` above it attached to the `set`, not the
`.`, so SC1090 fired anyway. Split across lines, the directive binds and
the script lints clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The comment claimed a bad key returns HTTP 200 with an error body, so
the status code alone proves nothing. Measured against the live API,
that is wrong: a bad key returns 401 and an unknown city 404, and with
curl -f both fail at curl without the .cod check ever running.
The claim came from the polybar script, which ran curl WITHOUT -f and so
genuinely did receive error bodies with a success exit. The check was
ported but not the reason for it. It still earns its place behind -f,
for a 200 whose body is not usable weather, and the comment now says
that instead of something false. The spec carried the same wrong
sentence and is corrected too.
Separately, the cache landed at 644 in a directory where every other
generated file is 600, and the response carries the configured city and
its coordinates. A umask before the write puts it at 600 without a
window where it is briefly readable.
The .cod discrepancy was found by the Task 6 implementer, which noticed
the bad-key test produced a different message than the plan predicted
and said so rather than reporting the expected text as achieved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Fetches current weather from OpenWeatherMap into a cache file, meant
to run from conky's ${execi 900 ...}. Writes to a temp file in the
same directory as the target and renames atomically, so a killed or
failed fetch never leaves the widget reading a half-written or error
response. Distinguishes HTTP failure from a 200-with-error-body (a
bad key returns the latter for some endpoints, the former here), and
leaves any existing good cache untouched on either kind of failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
is_stale and age_str were the only functions in the module doing
arithmetic on an unchecked argument: a nil `now` raised, and a raise in
this project is a blank dashboard with no message. Every caller passes
os.time(), which cannot be nil, so this was unreachable, but it is the
same bug already fixed once in icon() and the module header promises
nothing raises.
age_str also printed a negative age when the observation timestamp was
in the future, which a clock skew can produce. "stale -17m" on the card
reads as a broken widget, so the age now floors at zero and such a
reading counts as fresh: weather from the future is not old.
With this the whole module is raise-free, verified across every function
against nil, string, table, boolean, NaN, negative and zero arguments in
all three positions.
Both cases were found by the Task 5 implementer, which was asked to
probe beyond its given tests and reported them without changing the code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The numeric patterns stopped at the 'e', so a temp of 1.8e1 parsed as
1.8 and the card would have drawn 2 degrees instead of 18. Wrong in the
worst way available: silently, and still looking like weather.
OWM has not been seen to emit exponent form, and sampling several cities
near and below zero returned plain decimals throughout, so this was not
reachable in practice. Fixed regardless, because two characters in a
character class is cheaper than the reasoning required to be sure it
stays unreachable, and unlike the NaN case noted in sun_t this one fails
invisibly rather than leaving a mark.
Tests now cover both a negative temperature, which is ordinary here for
half the year, and the exponent form. Reverting the pattern fails them.
Found by adversarial probing of the committed parser, not by the plan's
own tests, which only exercise the fixture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Lua patterns instead of a JSON library: the current-weather response
is flat and known, so six scalars do not justify a dependency. Every
failure path (truncated write, empty file, garbage, an API error body,
nil input) returns nil rather than raising, since a Lua error here is
a blank dashboard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
NaN is a number, so the type guard admits it, and every comparison
against NaN is false, so neither clamp catches it and the function
returns NaN.
Left unguarded on purpose. No caller can produce it: parse() matches
%d+ for the sun times and `now` is os.time(). If one ever did, Cairo
ignores a NaN coordinate, so the arc would lose its dot rather than the
dashboard going blank. A ponytail: comment names the ceiling and where
the guard would go.
Found while probing the committed code, not by the tests, which do not
cover NaN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Two comments, no behaviour change.
`force - 1` reads as an off-by-one until you know the scale starts at 0
while ipairs starts at 1, so the loop index is already force + 1. Said
plainly now, along with why the fall-through returns 12.
compass() has no caller: the card draws arrow(). It is kept because it
shares arrow()'s binning exactly, so testing both pins that shared logic
from two angles, and switching the card to "S 9 km/h" becomes a one-word
change. Without this note the next reader finds an unused function and
has to reconstruct the reasoning.
Raised by code review as the only two points worth acting on; no
Critical or Important issues were found.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
icon(800, nil, 1500, 1900) compared nil against a number and raised,
which in this project is a blank dashboard with no message. The module's
own header promises nothing raises, and is_day() already guards the same
case, so the inconsistency was the bug.
Not reachable from the widget as written: it passes os.time(), and
parse() refuses a response without sunrise and sunset, so no caller can
currently produce the failing combination. Fixed anyway, because the
contract is what future callers will rely on and the fix is one word.
The test file also now says why the codepoint VALUES carry no assertion:
comparing M.ICON.snow against a literal would only prove the escape was
typed twice. Whether E31A draws a snow cloud is what the glyph sheet is
for, and mutation testing confirms the assertions cannot see it.
Found by code review. The same fix is applied to the plan, so Tasks 2-5
inherit the guarded version.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
DAY was 1000 against a sunrise of 1500, so every timestamp in the test
evaluated as night and the first day assertion could never pass. The
implementation was right; the test constants were not.
DAY is now 1700, inside the 1500..1900 window, with a comment saying why
the value has to be checked against the window rather than just named.
Caught by the Task 1 implementer, which stopped rather than quietly
inverting the day/night comparison to make the assertions pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Nine tasks, TDD throughout: the domain tables and parser get tests, the
card gets a screenshot.
The icon codepoints in the plan were each rendered and inspected rather
than derived. That caught U+E30A, which is present in the font and draws
a sun-with-rain, standing in for snow; the correct glyph is U+E31A. A
wrong-but-present codepoint fails this way, plausibly, which is why the
plan tells the implementer to look at the rendered sheet instead of
trusting a fontconfig query.
Task 6 and Task 8 both state which outcomes are acceptable when the OWM
key is still activating, so an inactive key blocks neither.
Paths are relative to the repo root rather than absolute: the first
version of this file hardcoded a home directory in eighteen places and
the pre-commit hook rejected it, correctly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
One vertical card merging idea1.png's two: conditions on top, a
sunrise-to-sunset arc at the bottom, with the times at the arc's ends
behind sunrise/sunset glyphs.
Decisions worth recording rather than rediscovering:
- The fetch runs from `${execi}` in conky.text, not a systemd timer, so
refresh is tied to the dashboard's lifetime. conky.text is empty in
this config because Cairo covers it, so whether execi fires at all was
probed rather than assumed: it does, even when the command prints
nothing.
- Inconsolata Nerd Font already carries the Weather Icons range and the
two sunrise/sunset glyphs, so no new font. Confirmed by rendering the
codepoints and looking at them; an fc-list query alone had wrongly
reported them missing, and a missing glyph in conky is an invisible
blank rather than an error.
- The cache is written atomically, since the widget reads it on an
unrelated 2s cadence and would otherwise parse a half-written file.
- A bad key returns HTTP 200 with a JSON error body, so the fetch checks
.cod before replacing the cache, and never overwrites good data with
an error.
- Missing and stale states draw the normal card chrome with a notice. An
empty cell was rejected: it looks exactly like a crashed widget.
Also ignores .superpowers/, the brainstorming session directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Hyprland's fullscreen rule resizes the window but not conky's drawing
surface, which stays at minimum_width/minimum_height. The dashboard
therefore painted its backdrop over roughly the top-left third of the
screen and left bare wallpaper beside it, visible as soon as the window
rules put it fullscreen on special:dash.
Set to the primary monitor, 2560x1080. The grid derives its cells from
the surface, so the layout still fills whatever size it is given; only
the surface needed to grow.
Task 4's development config keeps 1200x700 deliberately: a smaller
window is easier to screenshot and compare during that task, and it
never runs fullscreen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Install, how to edit the layout table, how to run the checks, and the
five gotchas that cost real time: conky_surface() on Wayland, a Lua
error presenting as a blank screen, own_window_type needing to be
normal, the palette being unreadable through conky_parse, and Cairo
having no Black font weight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Four defects, all found while running Task 8.
The documented toggle command does not work on this machine. Under the
Lua config `hyprctl dispatch` evaluates its argument as Lua, so the
plain `hyprctl dispatch togglespecialworkspace dash` fails with "')'
expected near 'dash'"; Hyprland even says so in its error. The working
form is `hyprctl dispatch 'hl.dsp.workspace.toggle_special("dash")'`,
verified toggling special:dash on and off. Also read the result from
`monitors[0].specialWorkspace.name`, since activeworkspace keeps
reporting the ordinary workspace underneath.
`fullscreen` and `no_focus` appear in no existing rule, so they were
checked against the runtime rather than left as caveats:
hl.window_rule rejects an unknown field with "unknown field '<name>'",
and neither produces that error, so both are accepted. A deliberate
bogus key confirmed the check itself works.
hl.mainMod is nil in any section other than keybindings.lua, which
declares `local mainMod = "SUPER"` and never exports it. The plan
hedged about this; it now states the literal to use.
Task 8's class check ran `conky -c ./conky.conf`, which is gitignored
generated output and absent on a fresh clone, so it now renders the
config first. Its `pkill -f` also matched the running script's own
command line and killed the shell's process group, returning 144 and
swallowing the log it had just been told to read; `pkill -x conky`
instead, fixed in Tasks 4 and 6 too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A Lua section, not a .conf fragment: the live Hyprland config is
hyprland.lua requiring sections/, with rules declared through the hl
helper API.
Pins conky-dash to its own special:dash workspace, fullscreen, no
border, no focus, and binds SUPER+S. Not SUPER+D, which keybindings.lua
already binds to the unnamed special workspace, and that workspace
already hosts the btop scratchpad.
no_focus matters because the dashboard starts with the session:
without it, it steals focus at login and can be tabbed to like an app.
autostart.lua already launches a bare conky, so the README says to
change that line rather than add one, which would run two instances.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The out-of-grid check iterated `G.layout or {}`. With layout unexported
that walks an empty table, so the block passed vacuously and the test
could never detect the very condition it existed to check. Confirmed by
unexporting layout and watching the suite still pass.
Drops the fallback and asserts the export first. Verified by mutation:
unexporting layout now fails with "dashboard.lua must export layout",
and moving the clock to col=5 fails with "layout entry out of columns",
so both paths have teeth.
The plan's Step 3 expected a failure that could not happen; it now says
what the failure looks like and warns that a pass there means the test
is broken rather than the code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The grid's justification is that one layout table works on differently
shaped screens, so assert it: every cell lands inside the margins at
2560x1080 and 1920x1080, a spanning cell absorbs the gap it covers, and
no shipped layout entry falls outside the declared grid.
dashboard.lua exports its internals for this; cairo and lib.card are
stubbed via package.preload since they only exist inside Conky.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The date rendered as "16 /SEP /2026", each slash jammed against the
next glyph. measure() returns the ink width, which ignores leading and
trailing spaces because a space carries no ink, and clock.lua was using
it to advance a cursor. Measured: " / " covers 6px of ink but advances
14px, so every separator lost 8px.
Splits the two uses that were conflated in one function. measure() is
for centring and right-aligning, where ink width is what you want;
advance() returns x_advance and is for laying out runs of text left to
right. The clock's date now uses advance() and reads "16 / SEP / 2026"
with even gaps, verified by screenshot.
Worth the split rather than changing measure() in place: text_right is
a correct caller of the ink width, so one function cannot serve both
and silently returning the advance would have broken alignment
instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|