Skip to content

4.3.29: Black layers, idle refresh-rate setting, copy layers from unavailable devices#206

Open
logicallysynced wants to merge 9 commits into
masterfrom
chromatics-4.x
Open

4.3.29: Black layers, idle refresh-rate setting, copy layers from unavailable devices#206
logicallysynced wants to merge 9 commits into
masterfrom
chromatics-4.x

Conversation

@logicallysynced

@logicallysynced logicallysynced commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Covers v4.3.25 through v4.3.29 on top of the 4.3.24 release.

Black layer type

"Black" is now available as both a base layer and a dynamic layer. The base variant holds the whole device at black, a dark foundation where only your dynamic layers light up. The dynamic variant holds the selected keys at black, a mask over anything a lower layer would paint. Neither reads the palette; black is the point, so there is nothing to configure beyond the key assignment.

Plumbing: DynamicLayerType.Black = 18 matches its position in _dynamicLayerOrder, BaseLayerType.Black = 6 appends to the enum, both processors register in LayerProcessorFactory, and layers.chromatics4 schemaVersion bumps 7 -> 8 in all three write sites. Older builds reset Black layers to None on load through the existing SanitizeLayerTypeIndexes guard.

Lower the RGB refresh rate while FFXIV is not running

New checkbox under Settings → General, on by default. The disconnect throttle already existed; this PR puts it behind a setting so users can turn it off and keep their configured rate at all times. Flipping the toggle applies the rate immediately.

A follow-up commit changed how the rate is decided: it now derives from the live game-connection state instead of a flag each caller passed. The old flag approach meant device hot-plug or re-enable while in game could re-apply the idle rate, so the effective rate depended on which code path ran last. With the setting off, the configured rate now runs in every state.

Copy layers from disabled and disconnected devices

The copy dialog lists devices that only exist in layers.chromatics4 (provider off, or hardware gone) as copy sources, alongside devices disabled on the Mappings tab. The picker labels their state - "(disabled)" or "(not connected)" - and a note under the source dropdown tells the user the copy runs from saved layers. A checkbox hides unavailable devices, and the destination list follows that filter too.

Disconnected devices are source-only, since a destination needs live LEDs to map onto. Their default LED mapping comes from a device-free LayerCopier core: identity match for keyboard pairs, ordinal fallback otherwise, with the per-layer mapping expander available to correct any row. Device names are not persisted with layers, so disconnected entries are labelled by device type plus an id slice.

Smaller fixes

  • The update prompt showed the newest version's heading twice and repeated older sections. publish.py now embeds only each version's own bullets, and the dialog trims old-format notes already on the update feed.
  • The "Pair" button in the Nanoleaf dialog read as the noun in Spanish, French, and Japanese. Corrected to each language's pairing verb.
  • Replaced the obsolete TextBox.Watermark with PlaceholderText (AVLN5001).

Notes

  • New locale keys shipped in all seven languages.
  • chromatics-docs updated separately (Settings, Mappings, and layer reference pages).
  • 173/173 xUnit tests pass, including new coverage for the LED mapping rules and the update-notes trimming.

🤖 Generated with Claude Code

…3.24 -> v4.3.25)

Idle refresh: the disconnect throttle (surface drops to the idle tick
rate while FFXIV is not attached, resumes the configured rate on
attach) is now gated by a new General setting, on by default. The
Settings toggle re-applies the rate immediately through
RGBController.ReapplyIdleUpdateRate instead of waiting for the next
connect or disconnect.

Copy layers: the copy dialog now lists devices that survive only in
layers.chromatics4 - provider off or hardware gone - as copy sources,
alongside Mapping-tab-disabled devices. Both carry a state label in
the picker ("not connected" / "disabled") and a notice under the
source row so the origin is obvious, and a checkbox hides them for a
shorter list. Absent devices have no IRGBDevice, so a new
LayerCopier.ComputeDefaultMappingForLayer overload derives the
default LedId mapping from the layer's own LedId set (identity for
keyboard pairs, ordinal fallback otherwise); they stay source-only
since a destination needs live LEDs. Device names aren't persisted
with layers, so absent entries are labelled by device type plus an
id slice.

New locale keys in all seven languages. 160/160 tests pass.
@logicallysynced logicallysynced self-assigned this Jul 18, 2026
…ng rules under test (v4.3.25 -> v4.3.26)

The hide checkbox only rebuilt the source list: when the selected
source survived the filter, the selection setter no-oped, the
source-changed handler never ran, and the destination dropdown kept
(or kept missing) disabled devices until the source changed. The
filter toggle now rebuilds destinations, revalidates the selected
destination, and rebuilds the layer rows whenever the source
selection carries over.

The absent-source default-mapping rules moved onto a device-free
core (source LedIds + destination LedId list) with the IRGBDevice
overload delegating to it, since constructing an IRGBDevice needs a
full device stack that kept the rules out of unit-test reach. Five
tests pin the behaviour: keyboard identity with no positional
fallback, exact-id match, ordinal fallback, tail dropping, and
empty-input handling.

165/165 tests pass.
Machine translation rendered the single-word key as the noun (a pair
of things) instead of the verb the Nanoleaf pairing button needs.
Corrected to match each language's own pairing strings: Emparejar,
Appairer, and the katakana pairing form. German, Korean, and Chinese
were already verbs. Locale-only change, no version bump.
…> v4.3.27)

The update dialog prepends its own "## {version}" heading per release,
but the notes publish.py embedded started with the version's changelog
heading themselves and ran to the end of CHANGELOG.md - so the prompt
showed the version line twice and stacked older sections into every
entry when listing target plus deltas.

Fixed at both ends. publish.py's extract_release_notes now captures
only the version's own bullets (heading excluded, stopping at the next
section), since the dialog assembles the cumulative changelog itself
from per-version asset notes. The dialog trims each release's notes
through a new UpdateService.TrimNotesToOwnSection before rendering,
which drops a leading heading naming that version (all changelog
heading shapes, 4-part tolerated) and cuts at the next section - a
no-op for new-format notes, and the compatibility path for releases
already published with the old format. Entries whose notes trim to
nothing are skipped instead of rendering an empty section.

Eight tests pin the trimming: old format reduces to own bullets, new
format passes through, every heading variant strips, a mismatched
heading is kept while trailing sections still cut, and empty input
returns empty. Also corrected a small grammar slip in the changelog
bullet wording. 173/173 tests pass.
Avalonia 12 deprecated Watermark (AVLN5001); the Nanoleaf adoption
dialog's manual-IP field was the only use. Same rendered behaviour and
the same locale key, so no version bump.
Both hold their keys at #000000: the base variant covers the whole
device (a dark foundation only higher layers light up), the dynamic
variant covers the selected keys (a mask over anything a lower layer
would paint). Neither reads the palette - black is the point, so the
colour never varies and there is nothing to configure beyond the key
assignment.

Wiring follows the layer-type invariants: DynamicLayerType.Black = 18
matches its appended position in _dynamicLayerOrder, BaseLayerType
.Black = 6 appends to the enum, both register in LayerProcessorFactory
(whose switches throw on missing entries), and layers.chromatics4
schemaVersion bumps 7 -> 8 in all three write sites with the version
history noting the new value space. SanitizeLayerTypeIndexes already
guards by Enum.IsDefined, so older builds reset Black layers to None
on load instead of dispatching a cast they cannot handle.

The ComboBox tooltip comes from the LayerDisplay description; both
descriptions and the shared "Black" name ship in all seven languages.
173/173 tests pass.
…lack tooltip (v4.3.28 -> v4.3.29)

The surface tick rate was set from a flag each caller passed:
RunStartupEffects passed idle:true, StopEffects(gameFirstConnected)
passed false. But RunStartupEffects also runs on device hot-plug and
re-enable while the game is attached, so the effective rate depended
on which path fired last rather than on whether FFXIV was connected -
which could leave the rate lowered at the wrong time. ApplyUpdateRate
now derives the idle state from GameController.IsGameConnected and the
setting, recomputed on every call and on the Settings toggle, so the
configured rate always runs while connected and whenever the setting
is off. Removes the caller-passed flag and the last-requested-state
field.

Also removed the "(#000000)" from the two Black layer tooltips per
request; the descriptions still say the keys are set to black. Locale
keys regenerated for all seven languages.

173/173 tests pass.
@logicallysynced logicallysynced changed the title 4.3.25: idle refresh-rate setting and copying layers from unavailable devices 4.3.29: Black layers, idle refresh-rate setting, copy layers from unavailable devices Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant