| Age | Commit message (Collapse) | Author | Files | Lines |
|
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>
|
|
Hour stacked over minute in Noto Sans Black, weekday in caps, date
slashed, following idea2.png. Colours are the palette's.
Numeral size derives from the cell rather than a constant, so the same
widget fills its cell on either monitor. The slashes take the dimmer
label colour so the numerals read first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
An earlier patch left "c8 = 200/255 alpha." on its own line directly
above the sentence that already says it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Same settings as the dev config with the colours as @ROLE@
placeholders, plus @CRITICAL@ for the error overlay, which gen_conky
does not substitute yet.
The rendered conky.conf is gitignored for the reason UDT ignores its
generated files: tracking it turns every scheme switch into a diff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Two defects found while running Task 4, both confirmed against the
conky build rather than assumed.
Task 4's development config defined color1..color7 and default_color
but not default_shade_color, which palette() reads for the card fill,
so following the plan verbatim drew every card in the magenta sentinel.
The config now carries it, with a note saying why it is required.
own_window_argb_visual has been REMOVED in conky 1.24 (ARGB is always
on when available) and own_window_argb_value is deprecated; both warn
on this build. Replaced with own_window_colour '#AARRGGBB', which emits
no warnings. In the template that value follows the palette through a
new @BODY_SHADE_RAW@ placeholder, the shade colour without its leading
'#', so a scheme switch recolours the window backdrop too rather than
pinning it to one hardcoded hex.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Entry point, grid maths, palette parsing and widget dispatch.
Uses conky_surface(), not cairo_xlib_surface_create: under
out_to_wayland the Xlib drawable and visual are both nil, so the idiom
every Conky-Lua tutorial uses cannot work here.
The pcall overlay lands before any widget exists, because Conky reports
a Lua fault as a blank screen with no message on any stream. Verified
by injecting a fault and seeing it reported. A missing widget names
itself in its own cell rather than leaving the cell empty.
conky.conf here is a development config with literal colours; the
templated one arrives with the UDT wiring.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Two defects, both found by review of the Cairo work and both confirmed
by experiment rather than reasoning.
The palette mechanism could not have worked. conky_parse('${color3}')
returns an empty string: conky's colour variables emit renderer escape
codes into conky.text, they never evaluate to a hex string, and
${default_shade_color} is not a variable at all, so conky_parse hands
the literal text back. conky.config is not exposed to Lua either
(_G.conky is nil) and conky_info carries only cpu_count and
update_interval. What Lua does get is conky_config, the config file's
path, so the colours are now parsed out of that file, where UDT has
already substituted real hex. The pattern needs [%w_]+ rather than %w+
or it misses default_color and default_shade_color.
The card fill also duplicated the border colour, both being color3, so
filling at 0.55 alpha and stroking the hairline at 0.9 in one hue made
the border invisible and the cards read as blobs rather than the
mockup's thin outlines. The fill is default_shade_color now, a role
every scheme already defines.
card.measure allocated a cairo_text_extents_t per call, which leaks:
5000 allocations grow Lua's heap by 182KB that GC never reclaims, and
:destroy() on each does not help, measured at the same 182KB. One
reused module-level struct costs 0KB. A draw hook running every 2s
would have bled memory for as long as conky stayed up, which no
screenshot would ever reveal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Rounded-rect card with a hairline border, text helpers with measured
alignment, and font selection.
Cairo's font API stops at BOLD, so the Black face is reached by family
name instead of weight: at size 90, '13' measures 100px as Noto Sans
Black against 94px as Bold, confirming the heavier face is selected.
The card radius is clamped to half the shorter side, because a larger
radius makes the corner arcs overlap into a bowtie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The test comment said "cpu_percent", but the API is new_cpu_counter()
plus :sample(). Prose only, the code was always consistent, but a
comment naming a nonexistent function sends the next reader looking
for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Load is a delta, so one reading cannot yield a percentage. The first
call returns nil rather than a fabricated number: 100% on startup reads
as a real spike.
A non-advancing counter also returns nil instead of dividing by zero,
and the result is clamped because a suspend/resume can produce a
nonsense delta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Parsers take file contents as a string, not a path, so they test
against committed fixtures with no filesystem mocking.
hwmon is globbed by its name file rather than a fixed index, carried
over from the old config: indices drift across kernel reorders and a
stale one silently reads a different chip.
Failure returns nil, never 0. A sensor reading 0 C is legitimate, so 0
cannot double as an error value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The live config is hyprland.lua requiring sections/, with rules
declared through an hl helper API, so the planned .conf fragment with
source = could not have worked. Task 8 now writes a Lua section using
hl.window_rule and hl.bind, matching the existing rules.
Three facts checked against the running config:
- keybindings.lua already binds SUPER+D to the unnamed special
workspace, so the dashboard takes SUPER+S.
- autostart.lua already runs a bare conky. The dashboard replaces it,
so that line changes rather than a new exec being added, which would
run two instances.
- The unnamed special workspace hosts the btop scratchpad, so the
dashboard gets its own special:dash.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Ten tasks, TDD where there is logic to test. Ordered so nothing
half-built touches the working desktop: pure Lua and its tests first,
then pixels, then the palette and the widget, then window placement,
and only last does UDT switch over to this repo's template.
Leads with the platform facts verified during design, because each one
invalidates the approach an implementer would otherwise reach for:
conky_surface() instead of the Xlib idiom on Wayland, own_window_type
normal rather than desktop, a Lua fault presenting as a blank screen,
grim capturing screen coordinates so it needs the dashboard's workspace
active, and Cairo having no Black font weight.
The pcall error overlay is built before any widget and verified by
injecting a deliberate fault, since it is the only channel that makes a
later mistake visible.
Fixture arithmetic in the parser tests is checked rather than asserted
from memory: a wrong expected value sends the implementer chasing a bug
that is not there.
Paths are ~-relative throughout, so the plan does not pin to one home
directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A polybar backup script for OWM turned up, which beats the weather.com
scraper: it has a working condition mapping. Port the domain knowledge,
not the code.
Worth keeping: the OWM condition-id ranges, day/night selection against
sys.sunrise/sys.sunset, and the Beaufort wind thresholds. Becomes a
table in data.lua.
Not portable: polybar %{F}/%{T} format tokens, hardcoded hex (UDT roles
replace them), bc conversions (Lua arithmetic), one-line output (a
card). So the script shrinks to curl-and-cache and Lua does the rest.
The reference script hardcodes a live API key plus city and country. The
key and location move to ~/.config/udt/weather.env, outside the repo,
same reasoning as roles.conf. Nothing carries a key in-repo, and
weather.env is gitignored so a stray copy cannot be staged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Conky dashboard drawn with Cairo from Lua, on a Hyprland special
workspace. Replaces UDT's variables-based conky.conf.in.
Platform facts verified before fixing the design, since each one
invalidates a common approach:
- Cairo on Wayland needs conky_surface(). The usual
cairo_xlib_surface_create idiom gets a nil drawable and visual under
out_to_wayland, so every tutorial using it is wrong here.
- own_window_type='normal' makes Conky a real toplevel that a Hyprland
windowrule can place; 'desktop' is a layer-surface and cannot be
assigned to a workspace.
- Cairo output covers conky.text, so conky.text stays empty.
Layout is a table of grid cells at the top of dashboard.lua, so
reordering widgets is one edit and the same table works on both
monitors. The 2560x1600 mockup's vertical stack does not fit the
2560x1080 primary, which is what motivates cells over pixels.
hwmon glob-by-name is carried over from the old config deliberately:
fixed indices drift across kernel reorders and silently read the wrong
chip.
v1 is one clock card end to end. Widgets are additive after that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|