| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
| |
configure_grub replaced the whole GRUB_CMDLINE_LINUX_DEFAULT line with a fixed string. A base install that had set cryptdevice=, resume=, zfs=, or any other boot-critical parameter lost it, and the grub-mkconfig that follows baked an unbootable config.
update_grub_cmdline now reads the current value and merges: every existing token survives, archsetup's tokens are added, and where both set the same key archsetup's value wins. A safety check refuses to write when any existing token's key would vanish from the merge, and the write goes through awk + mv so paths with slashes can't break the substitution. The block also backs up /etc/default/grub before editing, which the other system-file edits already did.
|
| |
|
|
|
|
|
|
| |
The Hyprland socket check passed a shell glob to `test -S`, which breaks at both edges: zero matches leaves the literal pattern and several instances pass multiple args, failing for reasons unrelated to the socket. It now resolves one socket with find -type s.
The gen_tokens degenerate-marker test asserted properties loose enough to pass on garbled output. It now pins the exact reconstruction as a characterization, with the impossible-input caveat stated, so any change to the branch surfaces.
The gallery tick count leaned on split-string omit-nulls arithmetic that happened to equal the match count. It now counts occurrences directly. gallery-widget-write-svg, the last uncovered public entry point, gets a Normal-case test.
|
| |
|
|
|
|
| |
The needle angle clamped to 0-100 while the readout rendered the raw value, so a gauge fed 150 pinned the needle at +60 degrees under a "150%" label. A shared gallery-widget--clamp-value now feeds both halves.
Two hygiene fixes ride along. The module requires cl-lib explicitly instead of leaning on svg.el's transitive require. The token-file load resolves its directory through gallery-widget--source-dir, which falls back to default-directory when the form is re-evaluated outside a load and outside a file buffer (both location variables are nil there and file-name-directory errored).
|
| |
|
|
|
|
|
|
|
|
| |
init_vm_paths suffixed the disk and NVRAM by FS_PROFILE but left PID_FILE, MONITOR_SOCK, and SERIAL_LOG shared. A concurrent btrfs and zfs run read each other's PID file, so the second run's stop logic could kill the first run's VM. All runtime paths now carry the suffix.
kill_qemu sent kill -9 and returned without waiting, so the force-kill fallback could run qemu-img snapshot against a qcow2 the dying qemu still held locked (the restore failed silently behind || true, leaving the base image dirty). It now reaps or polls the process to death before returning.
debug-vm.sh hardcoded the btrfs base-disk name, so FS_PROFILE=zfs booted the wrong base or fatalled. It now takes DISK_PATH from init_vm_paths.
Both runners reported a completion-marker grep as "ArchSetup Exit Code," but the installer runs detached with set -e off, so its real exit status was never captured -- a run that errored mid-way and still reached its last line reported exit 0. The flag is now ARCHSETUP_COMPLETED with the report labeled honestly; Testinfra stays the pass/fail authority.
|
| |
|
|
|
|
| |
The ordering asserts compared two string literals to each other ("05..." < "60..."), which is constant-true and never read the file. The invariant it claims to guard is boot-critical: pacman runs hooks in filename order, and a guard hook renamed past 60-mkinitcpio-remove would let a blocked transaction remove the current initramfs with this test still green.
The test now extracts the hook filenames the installer actually writes and compares those against the stock mkinitcpio hook name, so a rename flows into the assertion. Verified by mutation: renaming 05-zfs-snapshot to 70- in a source copy fails the new extraction compare where the old literal compare stayed true. Also added: every written hook must carry a two-digit ordering prefix.
|
| |
|
|
|
|
|
|
| |
The scenario_diagnose_expect else-branch printed its FAIL line but never forced a non-zero subshell exit. A scenario whose only failure was the diagnose check counted as passed, so the run printed "all scenarios passed" and exited 0 over a real net-doctor diagnosis regression.
The miss now sets a per-scenario rc that carries to the subshell exit. The fix and assert still run first, since the repair result is worth having either way.
The new harness drives the real script with stubbed ssh/rsync/jq and a controllable fake scenario, pinning all four check outcomes.
|
| |
|
|
|
|
| |
The nvme early-load edit landed in mkinitcpio.conf, but the only nearby mkinitcpio -P ran behind an is-not-zfs-root gate. On ZFS-root machines (all of mine) the hardening was never compiled into the initramfs, which is exactly the case the early load exists for. The extracted ensure_nvme_early_module now rebuilds whenever it changed the file, and skips the rebuild on an unchanged conf so a resume stays idempotent.
The already-present check also grepped the whole file for "nvme", so a comment or nvme_tcp anywhere skipped the edit. It now checks for the nvme word on the MODULES line only.
|
| |
|
|
|
|
|
|
| |
Four near-identical blocks validated that AUTOLOGIN, NO_GPU_DRIVERS, INSTALL_CLAUDE_CODE, and INSTALL_DEVICE_UDEV_RULES were empty or exactly yes/no. They collapse into a validate_yesno helper the callers guard with `|| exit 1`.
The NVIDIA driver floor of 535 was a magic literal in the comparison and three prose lines. It now lives in a NVIDIA_MIN_DRIVER constant, and the nvidia-preflight test sources that constant alongside the function it extracts.
This is the safe, purely-testable slice of the refactor rollup. The structural extractions that touch the installer's system-mutation paths -- the GPU modalias scan, the stow and retry loops, the display-server/window-manager dispatch, and the snapper/fsck/GRUB/waybar sed blocks -- are split into a follow-up so they get characterization coverage first rather than a blind rewrite.
|
| |
|
|
|
|
| |
The script re-encoded each notify sound with `cat "$tmp" > "$f"`, which truncates the target first -- a short or empty encode left a corrupt, repo-tracked sound file. The mktemp also had no trap, so an interrupted encode leaked a temp.
Now it resolves the real target with `readlink -f` (SOUND_DIR is usually the stow-symlinked ~/.local copy), stages the temp beside it, guards on a non-empty encode, and moves it into place. The mv is atomic on one filesystem and replaces the real file, so the stow symlink still points at it. An EXIT trap removes the in-flight temp when a failed encode aborts the run.
|
| |
|
|
|
|
| |
With `set -e` off, the installer copied its zfs-replicate, zfs-pre-snapshot, and hypr-live-update-guard scripts into place with bare cp/chmod. A missing or partial source failed silently, leaving a systemd service with a dead ExecStart or a pacman hook pointing at a script that never landed.
The two zfs scripts now install through install_executable, which guards the copy, warns on failure, and skips the chmod when the copy did not land. The two live-update-guard chmods (the guard binary and its hook) are guarded inline with error_warn.
|
| |
|
|
|
|
|
|
|
|
| |
Each of these steps misbehaves on a resume re-run or under unexpected system state, so I pulled the logic into small testable helpers and fixed the behavior.
The log-cleanup cron line was appended unconditionally. essential_services is one resume step, so a failure past that point plus a re-run stacked a second identical cron line every time. crontab_append_once now appends only when no existing line matches.
The weekly ZFS scrub timer used `zpool list | head -1`, which picks an arbitrary pool when several exist and builds the malformed unit `zfs-scrub-weekly@.timer` when none do. zfs_scrub_timer_units emits one timer per pool and nothing for no pools, so every pool is scrubbed and an absent pool warns instead of enabling a broken unit.
gamemode was enabled through `systemctl --user enable`, which the script itself documents fails at install time with no user session bus -- so gamemoded was likely left un-enabled behind a silent warning. It now wires the wants symlink by hand through enable_user_service, the same helper syncthing's block folds into.
|
| |
|
|
|
|
| |
`nmcli connection import` activates a full-tunnel (0.0.0.0/0) Proton profile immediately. The import loop renamed and set autoconnect off, then brought the tunnel down. Under `set -euo pipefail`, a failed modify aborted the script before that down ran, leaving a live full-tunnel VPN routing all traffic through Proton -- the exact unasked-for VPN the script exists to prevent. The next-run guard catches the leftover profile but not the still-active tunnel.
Moved the down ahead of the modify. It targets the UUID, which is stable across the rename, so downing first is safe and no failure between import and modify can leave the tunnel up.
|
| |
|
|
|
|
| |
The installer copied /etc/sudoers.pacnew over /etc/sudoers with no validation. A malformed pacnew that sudo refuses to parse would replace a working sudoers with an unparseable one, locking out privilege escalation right before the NOPASSWD rule gets appended. replace_sudoers_pacnew now runs `visudo -cf` on the pacnew first and only copies a file that validates; a failure warns and leaves the current sudoers in place.
Extracted the logic into a function taking the pacnew and target as positional args defaulting to the system paths, so the guard can run against fixtures.
|
| |
|
|
|
|
| |
run_step marked a step complete only when its function returned 0. But error_fatal exits the script outright, so a step function that returns at all has already cleared every fatal check -- a non-zero return can only be a trailing non-fatal warning, since error_warn returns 1. The old code read that as a failure and withheld the state marker, so the step re-ran on every resume even though its body had finished. run_step now records the marker whenever the function returns and logs a warning line when the return was non-zero.
Two smaller fixes alongside it. run_step's step_name/step_func and show_status's step/timestamp now use local so they stop leaking to global scope. The package-cache refresh loop captures pacman's real exit code instead of reading `$?` off the `$refresh_ok` test, which always read 1 and made error_fatal report "error code: 0" -- the same trap retry_install already documents.
|
| |
|
|
|
|
|
|
| |
`df /` wraps to two lines when the root device name is long -- a device-mapper or live-ISO root -- so `awk 'NR==2 {print $4}'` read the wrapped device line, found an empty Available field, and aborted a valid install with "Insufficient disk space." `df -P` forces POSIX single-line output.
The gate now compares available KB against the minimum in KB rather than truncating to GB first, which rejected a disk sitting just under a whole-GB boundary. Non-numeric df output falls back to zero so a malformed read aborts loudly instead of crashing the arithmetic test.
Extracted the logic into check_disk_space so it can be exercised directly.
|
| |
|
|
| |
set -e is off, so a chpasswd failure silently left the primary user with no password and no log entry, leaving an unloggable account on a fresh install. I extracted set_user_password, which aborts via error_fatal on a chpasswd failure. A fake-chpasswd test pins that the guard fires.
|
| |
|
|
| |
gpg writes its output at the process umask (often 0644), so the plaintext mail password was world-readable between the decrypt and the chmod. I extracted decrypt_to_secure, which runs the decrypt in a 0077 umask subshell so the file is 0600 from creation. A fake-gpg test pins that the write runs under the tight umask.
|
| |
|
|
| |
TRAMP's /ftp: method shells out to a command-line ftp client, and Arch ships none by default. inetutils provides /usr/bin/ftp, so I added it to install_required_software. A static test asserts it stays in the base set.
|
| |
|
|
| |
Install snapshot and live-update hooks before mkinitcpio removal, migrate legacy paths, and cover the ordering invariant.
|
| |
|
|
|
|
|
|
| |
outputWell, toast, annunciator, tapeCounter, freqScale, patchBay, dataMatrix, warningFlag, seg14, and responseGraph each get the contract comment. The six DOM builders take dupre- prefixes across their markup and CSS blocks. patchBay's live jack and the annunciator's master caution adopt the kit-wide dupre-hot and dupre-on modifiers, and the annunciator board takes dupre-warn and dupre-fault. freqScale reuses the shared scoped dupre-tick and dupre-mk (as the tuner and dualknob do) and compounds its unit label to dupre-fs-band, since dupre-band is the EQ's. The four SVG-stage builders style through the shared .rsvg stage and inline attributes, so they get the SVG-built contract comment and no prefixes.
The annunciator documented a 0..2 state per cell but painted an undefined class on an out-of-range initial state or set(). It now clamps at build and at set. Two probe checks cover the batch: one fails red without that clamp, one drives each renamed DOM builder through its handle to catch a class-name mismatch the exception scan can't see.
Extraction audit: 104/111 contract comments.
|
| |
|
|
|
|
|
|
|
|
| |
roundCrt, attitudeIndicator, headingBug, chartRecorder, verticalTape, twinNeedle, comfortMeter, badges, tabularReadout, and engravedLabel each get the contract comment. The three DOM builders take dupre- prefixes across their markup and CSS blocks: badges (dupre-badge with the kit-wide dupre-red plus a new dupre-ghost), tabularReadout (dupre-readout and dupre-unit), and engravedLabel (dupre-engrave and dupre-cnt).
The rename surfaced a dead rule: .readout .u selects a descendant, but the unit caption is a sibling, so its style never applied. The caption now styles through a standalone .dupre-unit rule.
The builders now enforce their documented domains at build, not only in set(): twinNeedle and comfortMeter clamp their initial opts through set(), headingBug normalizes opts.value to 0..360, and badges coerces junk variants through one norm() shared by build and set(). Two probe checks cover the gates and drive the renamed DOM.
Extraction audit: 94/111 contract comments.
|
| |
|
|
|
|
|
|
| |
crossNeedle, thermometer, bourdon, stripChart, corrMeter, battCells, mcVu, chromeMinMax, aviationGauge, and edgeMeter each get the contract comment. The six DOM builders take dupre- prefixes across their markup and CSS blocks. Internals go compound where gauge, fader, and nixie already hold the plain names: bourdon's needle, hub, and cap, corrMeter's face, arc, needle, and label, crossNeedle's arc, and thermometer's tube and fill. crossNeedle's nA and nB become dupre-fwd and dupre-rfl, and battCells moves its cell modifiers to the shared dupre-on and dupre-warn.
The handles now enforce their documented domains: the needle meters clamp set() to 0-100, stripChart floors its history at two samples and survives a non-array set(), battCells floors its cell count at one, and mcVu clamps and paints opts.value at build instead of ignoring it for a hard-coded .35. Two probe checks cover the gates and drive the renamed DOM.
Extraction audit: 84/111 contract comments.
|
| |
|
|
|
|
|
|
| |
decadeBox, twoHandSafety, voiceLoop, miniSig, fuelBar, radialRing, sparkline, waveStrip, scope, and eqBars each get the contract comment. The eight DOM builders take dupre- prefixes across their markup and CSS blocks. voiceLoop's inner bar becomes dupre-vk-bar to stay clear of fuelBar's dupre-bar, and miniSig and eqBars move their cell modifiers to the shared dupre-on, dupre-hot, and dupre-clip.
Domain gates land where a handle could wedge: decadeBox coerces and clamps its digits, twoHandSafety ignores sides other than L and R, voiceLoop keeps the demo state to the stock loop set (a caller's shorter list used to crash on it), the meters clamp set() to their documented ranges, and sparkline and scope guard the one-sample case that drew NaN coordinates. Two probe checks cover the gates and drive voiceLoop's grammar through the prefixed DOM. probe-fams follows scope's rename.
Extraction audit: 74/111 contract comments.
|
| |
|
|
|
|
|
|
| |
timerDial, rockerPad, fourWayToggle, pinMatrix, deadMan, telephoneDial, breakerPanel, dsky, camTimer, and knifeSwitch each get the contract comment. dsky, the one DOM builder, takes dupre- prefixes across its markup and CSS block. That resolves the batch-1 deferral on the shared .win name, and its lamp and window modifiers move to the shared dupre-on and dupre-hot.
Domain gates land where a handle could wedge: fourWayToggle ignores anything outside its four quadrants (prototype-inherited keys included) and falls back to C on a bad initial position, camTimer's set() clamps to the ring, and pinMatrix drops initial pins that name no intersection. Two probe checks cover the gates and drive dsky's grammar through the prefixed DOM.
Extraction audit: 64/111 contract comments.
|
| |
|
|
|
|
|
|
| |
entryKeypad, thumbSlide, waveRegion, ledRow, pillSlide, spunKnob, stompSwitch, wingSelector, discSwitch, and guardedToggle each get the contract comment. All ten are SVG-stage builders, so there were no classes to prefix.
set() hardening lands where a handle could wedge: ledRow, pillSlide, and discSwitch clamp their index (discSwitch previously threw), waveRegion keeps E at least 4 above S from set() itself rather than only the drag path, and entryKeypad ignores keys outside its documented 0-9/confirm/clear set. ledRow's default selection is index 5 only for the default program list. A caller's own items start at 0.
Extraction audit: 54/111 contract comments.
|
| |
|
|
|
|
|
|
| |
dipBank, jogWheel, vernierDial, batToggle, flutedKnob, filterBank, chickenHead, slotFader, spadeKnob, and multiBandDial each get the contract comment. The two DOM builders also get dupre- prefixes on their classes (dip/dipsw and the on modifier, jog/shuttle/inner/dimple). jogWheel caches its inner disc instead of querying it on every move.
set() hardening lands where a handle could wedge: vernierDial and flutedKnob clamp 0-100, filterBank clamps the band index and its initial values, and multiBandDial clamps the band and keeps the current one when set() omits it (a bare set(v) previously threw).
Extraction audit: 44/111 contract comments.
|
| |
|
|
| |
rotarySelector, slideRule, rocker, transport, presetBank, dualKnob, encoder, keySwitch, crossfader, and thumbwheel get contract comments, dupre- prefixed CSS, and cached element refs. presetBank and dualKnob set() now clamp out-of-range input, with a probe check covering both. rotarySelector and keySwitch cap their inputs at the five- and three-stop plates they draw. The audit's contract count moves to 34/111.
|
| |
|
|
|
|
|
|
| |
Batch 2 of the extraction sweep: consoleKeys, abcKeypad, indexPlate, faderH, faderV, knob, segmented, chipToggle, armButton and lampRow now carry the split-flap-shape contract comment. Every class they own wears the dupre- prefix, internals and modifiers included (dupre-key dupre-green, dupre-fader dupre-cap), with every in-kit consumer updated in the same pass: the dsky and annunciator pads, the rotarySelector and encoder spindles, the page's size bar, and the probe selectors.
I also removed indexPlate's dead '#dupre-defs' lookup (nothing ever created that id) and dropped the unused .key.off rule. The faders, knob and lampRow now cache their element refs instead of re-querying every set(), and segmented and lampRow clamp set() the way consoleKeys does.
The audit's contract count moves from 14 to 24 of 111.
|
| |
|
|
|
|
|
|
|
|
| |
Each builder now carries a contract comment in the splitFlap shape (opts, handle, where its CSS lives). I prefixed every batch-owned class with dupre- so page chrome can't collide with instrument internals. The audit's C1 moves to 14/111, the .txt collision is gone, and .on clears as its owners' batches land.
jewels also gets a real handle. Its state lived only in classList/--jc, so nothing could read or drive the lamps (a dead end for the waybar port). I moved state into the builder, made the DOM a paint of it, and added get/set.
lampRow and outputWell consume the shared lamp class, so they map their state fragments to the prefixed names internally. Their opts APIs are unchanged.
audit-extraction.mjs is the report-only grader for the bar (contract comments, page-styled internals, page reach-ins). It gates nothing until the sweep reaches 100%.
|
| |
|
|
|
|
| |
The gallery and its library are now The Dupre Kit, named for the dupre color theme. GW was too opaque to remember. The stylesheet id is now dupre-css, the card handle property is .dupre, and page prose says instrument instead of widget. File names stay put. Renaming them is a separate decision.
Historical log entries in todo.org keep their GW references as a record.
|
| |
|
|
| |
The default skin is the white card with dark lettering, not white ink on black, and the default face is Helvetica. Both list first in their chip rows with the alternatives a click away. The base flap rate moves to 100ms. The probe pins all three defaults, and its cascade windows widen to match the slower base so worst-case travel keeps its margin.
|
| |
|
|
|
|
|
|
| |
The split-flap's default skin becomes the white ink, listed first in the chip row, with the kit cream one click away. The base flap rate moves to 85ms, and the probe pins both defaults so a drive-by change can't silently flip them.
Every card's ID badge is now a real anchor to its own card, so a click deep-links the URL and a right-click copies it. One shared rule keeps the badge look.
The refactor sweep for extraction-readiness closes out: the flip duration and the ms clamp each live in one place, grid normalization is one helper shared by setText and the silent first paint (which now records itself as the last commanded grid, ending the null fallback), the state declarations sit above everything that references them (the same declared-later shape that caused a load crash earlier in the branch), and a stale two-skin comment caught up with the four skins. Behavior-preserving: all 79 probe checks stay green.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
skins, faces and a flap-rate slider
The N20 card becomes a three-row, six-cell departure board. Every advance sends each row to a completely different word, drawn at random from a 24-word pool with rows kept mutually distinct, and each cell runs at its own mechanical rate (a fixed 0.8x-1.35x of the base flap time), so letters and whole words finish at genuinely different times. A slider on the card sets the base rate (setFlapMs on the handle). The board advances only after it has settled and dwelt two seconds on the reading: the builder announces settle once per command (onSettle) and the page schedules the next advance from that signal, since a fixed-interval ticker would retarget mid-cascade and the panel would never rest. The chain stays gated off under reduced motion, where the board paints once and rests.
Customization lands as two construction axes on the chip rig: skin (cream-on-black default, true-white ink, the ivory board, or white cards with dark lettering) and face (Berkeley Mono or Helvetica, the grotesque real boards wore). The skin chips are miniature flap cells, a letter in the skin's real ink on its real board colour, and the font chips letter themselves in their actual face, so every choice previews itself. The flap-rate slider rides in the chips row, and the card's amber word readout is gone: the board is the display. The deterministic seam for callers and tests is setText; randomness lives only in the demo path (next/scramble), which re-aims against the last commanded grid rather than in-flight glyphs.
The settle work caught a real ordering bug: setText assigned each cell's target and ran it in one pass, so a cell finishing synchronously saw its neighbours' stale, already-satisfied targets and announced a partial board as settled. Targets now all land before any cell runs. Review caught two more: the dwell chain had escaped the reduced-motion gate (the board self-advanced every two seconds forever under reduced motion), and the skin chips rendered blank because the STYLES entries were strings where the rig paints an object's dot swatch.
Since this is heading for extraction as a regular component, the builder got the full refactor pass: a contract comment covering every opt and the handle surface, small named helpers, no page globals, and all CSS in the one split-flap block. The same bar is filed as a sweep task for every other builder. Probe coverage grows to 78 checks: the reduced-motion rest, the live dwell measurement, the scramble contract, both style axes, the speed api and the swatch legibility.
|
| |
|
|
|
|
|
|
| |
The card's flip was a 0.3s drop-fade that teleported each cell straight to its target character — a suggestion of a split-flap, not one. Since the gallery card is the spec the ports get measured against, the fake would have propagated.
Three behaviors replace it, studied from the HotFX split-flap component and re-derived (its repo carries no license, so no code was copied). The charset is the drum: opts.chars declares the flap order and a changed cell steps one flap at a time through intermediates, never jumping, so cells arrive staggered by travel distance. A set() mid-cascade re-aims the running cells at the new target. The fold is real: two half-panel animations per flip — the current top falls, the next bottom lands — over four clipped layers with backfaces hidden, replacing the drop-fade keyframe.
animate:false (the reduced-motion gate) collapses every move to an instant jump. The handle grows setText, chars, and reading() so probes can sample the displayed state mid-cascade; the default width goes 3 to 4 cells, which stops truncating every demo word. Probe checks were written red first: cascade arrival, intermediate readings, one-flap-at-a-time stepping in charset order, mid-cascade retarget, and the instant animate:false path.
|
| |
|
|
|
|
|
|
|
|
| |
Every widget's colour is either the consumer's to pick or locked for a reason, and I kept classifying cards wrong from memory during the colour pass. So the policy is a declared, checked property now. Six kinds. Accent and screen are free and take colour chips. Coded, emissive, relational and material are locked and must not. Each classified builder carries a POLICY record naming the kind, why this widget is bound that way, and what it may change and stay authentic (the range that actually existed, like the CRT phosphors that shipped). Eight are classified. The rest are the worklist.
The page shows all of it from that one source. A card recovers its builder from the arrow it was handed and reads GW.<builder>.POLICY, so the badge, the spec-sheet catalog and the index tally can't drift from the code. A probe holds it both ways: the recolour mechanism and the accent kind imply each other, so a locked card can't grow chips and an accent card can't forget to declare.
The chip toggle is the first consumer of a shared accent family (red, amber, green, white, vfd) from GW.accentStyles, so the next card that wants those five gets them from one definition rather than a copy.
Two smaller changes ride along because they touch the same header. The size toggle becomes WIDGETS/ROW, an explicit 1-4 column count instead of S/M/L zoom, and the policy panel sits level-right of the index.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The oldest way to type without a keyboard, and the first card where selecting and committing are separate acts. One hand walks a stylus over a printed plate, the other pulls a lever, and only the lever prints. You can hunt across every character and nothing happens until the second hand moves. Every other control in the kit fires on contact.
The keyboard follows the same rule: typing selects, Enter is the lever. A keypress that printed would make this the ABC keypad with a nicer plate, which is the one thing it must not be. It also does no case folding, where the keypad must. This plate carries both cases as distinct cells, so Shift does exactly the work a shift key would on a machine that hasn't got one.
KEYS and ACTIONS are derived from the layout table rather than kept beside it, so rewriting the table relays the plate, the keymap and the allowlist together. A binding aimed at a character the plate no longer carries isn't expressible.
After the AEG Mignon Model 4, keeping the two things worth keeping: the extended set a keyboard skips, and both cases with no shift key. Its frequency order goes. Reading the plate is the interaction, and P U G Q / V I N A B isn't something you can read your way around, so ours is alphabetical with capitals beside lowercase at the same column offset. I expect to revisit the keys, so the layout is a table the geometry computes itself from. I built it at four, five and nine rows to check the gutter survives a rewrite in either direction.
There's no space cell yet, so Space isn't ours and still scrolls the page. The real machine has a separate space key.
|
| |
|
|
|
|
| |
probe-fams.mjs had no try/finally, so a throw skipped chrome.kill() and left the headless browser alive on its port. The next run connected to that survivor and reported results from a stale page: old widgets.js, chips already clicked. It cost me a debug loop today, insisting a card's default ink was green and another's chips were missing, neither of which was true of the page on disk.
A probe that answers from the previous run is worse than one that crashes. Its two siblings already had the guard.
|
| |
|
|
|
|
|
|
|
|
| |
Twenty clicks for a passphrase was always the wrong answer. The plate takes focus on click and reads the keyboard now: lowercase normalises, Space maps to SPC and shows as ␣, Backspace to DEL, Enter to ENT. Every key routes through the same press() a click does, so the two paths can't drift.
press() is where the filter belongs, not the keydown handler. I had it in the handler first, which guards the web and leaves the Emacs port wide open, since that port installs KEYS into a keymap and calls press directly with no handler in the stack. It gates on ACTIONS rather than KEYS because they're different sets: CLR is a real key on the plate that no keystroke reaches.
The focus ring uses :focus, not :focus-visible. Chrome won't match :focus-visible on a mouse-driven focus of a non-text element, and clicking the plate is how it takes focus, so the ring would have shown only when tabbed to. On a 110-card page that means typing into a card with no sign it was listening.
The window is a screen like the scope's or the marquee's, so it takes the screen families through the --scr-* vars with the shipped colours as fallbacks: nothing moves until a chip is clicked. Glass and ink both recolour, because a screen that changes its text and keeps its backlight isn't a screen. It offers all six families where its siblings carry five, since a passphrase window has no reason to prefer one phosphor.
|
| |
|
|
|
|
|
|
|
|
| |
The kit entered digits three ways and text no way at all, so the panels that need a WiFi password had nothing to reach for. R57 takes free alphanumeric text. A-Z is laid out alphabetically rather than QWERTY, the industrial convention wherever the operator isn't assumed to touch-type, and what makes this a faceplate instead of a keyboard.
I followed two reference plates and departed from both, arguing each departure at its site. Their letters are blue and the kit has no blue control colour, so letters take the standard pale cap and digits a darker one: the photos' two-tone grouping without the foreign hue. Letters sit left and digits right so the alphabet reads down one unbroken block. With the digits on the left it stops column-aligning with itself halfway down.
DEL is new, because without a backspace one mistyped character costs the whole entry. It sits beside the digits in amber while CLR is exiled to the corner in red. DEL is constant and costs a character, CLR is rare and costs everything, so the safe key gets the good spot and the colour grades the cost before the legend is read. CANCEL is gone: on a plate that is the whole terminal it has a transaction to abandon, but here it did exactly what CLR does.
The window draws spaces as ␣. SVG collapses whitespace, and past the truncation boundary there are no pad dots left for a space to displace, so SPACE was a keypress with no feedback at all. The buffer keeps the real character.
|
| |
|
|
|
|
|
|
| |
Console keys read SCAN before LIVE, and LIVE is green. The palette already defines --pass as "run lamps, gear greens, monitor bars, LIVE lamps", so the widget contradicted the language it draws from. Its note asserted the old colours too. The per-key `red` boolean became a `tone` field, since a boolean can't express three states.
The slide toggle gains red and warn on its `on` axis, so the engaged state can be the notable one (mute, record, airplane mode). Red was reachable only on the `off` axis before, which paints the disengaged state: the opposite claim. A new onText axis makes the ON ink reachable at all. `on` had bundled background and ink together, so "dark pill, green legend" couldn't be expressed.
Presets name a combination (panel, run, armed, caution, dark), which is what a consumer reaches for. The axes are how one gets built.
|
| |
|
|
|
|
|
|
| |
The validation walk's per-card state lived only in browser localStorage, so a cache clear would lose it and the other machine never saw it. VSTATUS now carries the record in source, read as a fallback behind localStorage. The live walk wins on the machine doing it, and a deliberate un-check beats a baked green, because 'off' is a stored value like any other.
I build the export's text by hand rather than through JSON.stringify, which hoists canonical integer keys ('12' ahead of '01') and would reshuffle the whole block on every bake.
Copying it out never touches the clipboard. navigator.clipboard is denied outright on file://, and execCommand returns true while writing nothing. So the export lands in a selected textarea instead: a selection is readable as the PRIMARY selection, and that's the one path that works here.
|
| | |
|
| |
|
|
| |
builders
|
| |
|
|
| |
The VU ballistics chase stays page-side and drives mcVu.set(t); the R17 trace animation is widget-owned so it moves into the builder with its reduced-motion gate. The R17 face gradient keeps instance-local defs because its stops read screen-family vars from the widget subtree; probe-fams selectors follow the retired rcrt ids to the card handle.
|
| |
|
|
| |
Entry keypad, thumb-slide pair, waveform region editor, drum roller, LED program row, three-position slide, spun knob, and stomp switch. The drum roller's per-instance clip paths get a uid() helper so two instances can coexist; the region editor's screen-family chips now reach the widget through the card handle (card.gw.el) instead of a stage id, and probe-fams drives its post-recolor check through the handle's set(). Behavioral pass now 78 checks; both probes green.
|
| |
|
|
| |
The committed probes asserted the pre-sprint gallery: default size 3 (now 2/M since 16da33e) and five .famchips rows (card 01's option capsules reuse the class, so six exist). The zoom test now clicks 3x explicitly instead of assuming it as the default, and the family count filters to rows labeled 'screen' so future option-group cards don't break it.
|
| |
|
|
| |
The full-regression and screen-family probes lived in session scratch; the widgets.js extraction uses them as its per-batch no-regression gate, so they become repo tooling.
|
| |
|
|
| |
The generated file declares (provide 'gallery-tokens), but `require` resolves a feature by filename, so as tokens.el it could never load from a load-path. The name now matches the feature. gen_tokens.py, the renderer's load, and the README follow.
|
| |
|
|
|
|
|
|
|
|
| |
gallery-widget.el is the proof-of-concept Emacs renderer: it reads the generated tokens.el and renders the needle gauge (card 10) as SVG via svg.el. Rasterized through librsvg (the renderer Emacs itself uses), it matches the web card side by side: same arc, ticks, glowing amber needle, half-dome hub, and readout from the same tokens. That closes the riskiest unknown in the three-target plan, so the component pattern can now scale widget by widget.
The comparison surfaced a real gallery bug: the web gauge's ticks were invisible. Their transform-origin put the rotation center 40px below the dial, so the rotated ticks landed outside the clipped area. I moved the ticks to the arc's top edge with the origin at the pivot, and all three now show at -60/0/+60.
The parseability test caught a second bug: the mono token's font stack carries double quotes, which broke the font-family XML attribute. The renderer swaps them to single quotes.
Tests are ERT (tests/gallery-widgets/), covering token resolution, angle math (normal/boundary/error), and the rendered document's structure. make test-unit now runs the elisp suites alongside the python ones, and make test-elisp runs them alone.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The colors, glows, gradient ramp, and pulse rate were hardcoded literals scattered across ~40 sites in the gallery. Retuning the amber meant a find-and-replace through the whole file. I pulled them into tokens.json as the single source.
gen_tokens.py reads that source and emits three targets: web CSS custom properties (into the gallery :root, between markers), waybar GTK @define-color declarations, and an elisp alist for the future svg.el renderer. The three genuinely differ (CSS --vars with rgb-triple glows, GTK @define-color with underscore names, elisp hex alist), which is why one generator beats three hand-maintained copies.
The amber hue is defined once, and its glow triples are derived from the hex, so retuning it means editing one line and rerunning the generator.
The gallery render is unchanged. I verified it pixel-identical against the prior commit with reduced-motion forced and the live clock masked (diff of 0).
Tests cover hex conversion, the three emitters, marker replacement, and idempotency: 27 tests, 100% line coverage, wired into make test-unit.
|