Files
community-plugins/ds4-color
5fb0e0f279 Add DS4 Color plugin (Hy4ri/ds4-color) (#52)
* feat: add PS4 Colors plugin (DS4 lightbar control)

Wraps the ps4-colors CLI (github.com/Hy4ri/ps4-colors) as a Noctalia
plugin: bar button + color panel (native picker, hex field, presets)
that sets the DualShock 4 lightbar via the ps4-colors binary.

NOTE: thumbnail.webp still pending (generated in noctalia thumbnail
generator) — CI will flag the missing asset until added.

* fix: add PS4 Colors thumbnail (solid crimson card)

960x540 WebP, deep crimson (#990000) to match the plugin accent.

* feat: left-click applies saved color, panel adds Save button

- widget: left-click applies the saved color via service; right-click opens panel
- service: add 'save' IPC (persist without touching controller)
- panel: Save (persist) + Apply (set now) buttons
- translations: add 'save' key

* refactor: implement DS4 protocol in pure Lua (drop C dependency)

The plugin now writes the HID output report to /dev/hidrawN directly via
noctalia.writeFile. USB report 0x05 (32B) and Bluetooth report 0x11 (78B)
with CRC32 are built and the CRC verified against the kernel algorithm.
Device detection scans /sys/class/hidraw uevent for Sony DS4 PIDs.
No external binary or library required.

* fix: panel renders black box (ipairs(nil) at script load)

Move preset-click handler registration into onOpen (after presets are
loaded) instead of module top-level, where ipairs(nil) threw and aborted
the whole panel entry. Guard presets with 'or {}'. Replace unsupported
width='100%' string with a numeric width on the swatch.

* fix: match color_picker panel schema exactly (drop invented props)

Remove border='primary' (unknown value -> whole tree abort) and wrap=true.
Swatch now uses border='outline' + borderWidth to show selection, mirroring
oldirtty/color_picker. Aligns every ui.* node with a known-good panel.

* fix: service dead on load (pluginDataDir nil concat) + real device write

- Resolve pluginDataDir lazily + nil-guard lastFile() so the service entry
  actually loads (top-level concat on nil threw -> no onIpc -> no apply ->
  no notification, which is why the color never changed).
- setLightbar tries noctalia.writeFile, falls back to a python3 O_WRONLY
  binary write for the /dev/hidrawN char device (writeFile is file-oriented).

* fix: replace Luau bitwise ops (&/|/~/>>) with math-only helpers

Noctalia's Lua is strict (PUC-Rio semantic), not Luau: '&', '|', '~', '>>'
are syntax errors -> service failed to load at crc32_init -> no apply.
Added band/rshift/bxor/bnot32 math helpers; CRC32 + BT report now use them
and still produce the verified CRC 523bce6f.

* fix: panel black box (readonly _G) — use static onPreset1..9 globals

Noctalia's _G is immutable at runtime, so assigning _G['onPreset'..i] in
onOpen threw 'attempt to modify a readonly table' and aborted the panel.
Presets are a fixed list, so define onPreset1..9 as plain module globals
calling a shared applyPreset(i). Remove dead makePresetHandler.

* fix: drop unsupported textAlign prop on ui.input (ui-tree warning)

'textAlign' is a label prop, not an input prop; Noctalia ignored it with a
[WRN] ui-tree log line. Input defaults to left align anyway.

* fix: use device-gamepad-2 icon for catalog + bar glyph

'controller' glyph was missing; swap to device-gamepad-2 for both the
plugin catalog icon and the bar widget default glyph.

* rename: PS4 Colors -> DS4 Color (id Hy4ri/ds4-color, dir ds4-color/)

Rename plugin id, display name, bar glyph default, translation key
(ps4_colors -> ds4_color) and all internal IPC references. Drop the stale
ps4_colors_missing string (plugin has no external binary dependency).
Keep accurate comments crediting the original ps4-colors C source.

* ui: shorten panel (height 520->400, tighter gaps/padding, smaller swatch)

Save/Apply buttons were already the last row (bottom of the column); this
just compacts the overall panel height.

* art: use panel screenshot as thumbnail (960x540 webp, crimson bg)

* fix: lowercase author in id (CI validate: author must match ^[a-z0-9][a-z0-9._-]*$)

'Hy4ri' -> 'hy4ri' in id, author, and all internal IPC references.

* fix: thumbnail

* fix(review): declare python3 dependency + document hidraw fallback

Reviewer flagged: plugin declared dependencies=[] but the hidraw write fallback
invokes python3, so systems without Python cannot apply the color if the
direct writeFile fails. Declare 'python3' in dependencies, note it in the
manifest description, and document the requirement in README Requirements.

* security(review): validate color as 6-hex at every trust boundary

Reviewer flagged: last.json value entered shared state unvalidated, then got
single-quote-interpolated into /bin/sh -c via runAsync -> shell injection.

- Add isHex6() (exactly ^[0-9a-fA-F]{6}$) and enforce it at: loadLast
  (poisoned last.json), onSubmitHex, onApply/onSave (from state), onIpc
  apply/save, and the apply()/save() service funcs.
- Since only 6 hex chars can reach runAsync, the single-quote interpolation
  is provably safe (no quote-breaker / metachar can survive).
- Also fixes a latent bug: the old %x regex captured only 5 hex, rejecting
  valid 6-char colors like 990000.

* Update plugin description for clarity

Removed mention of 'hidraw write fallback' from the description.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Lemmy <studio@quadbyte.net>
2026-07-19 08:39:42 -04:00
..

DS4 Color

Set the LED lightbar colour on a connected PlayStation 4 DualShock 4 controller from a Noctalia bar button and a color panel. Click the bar icon to apply your saved colour, or right-click to open the panel and pick a new one.

The entire DualShock 4 output-report protocol (USB report 0x05, Bluetooth report 0x11 with CRC32) is implemented in pure Lua — no external binary or library is required. The plugin writes the HID output report straight to /dev/hidrawN.

Plugin

Field Value
ID hy4ri/ds4-color
Entries Bar widget: widget; panel: panel; service: service

Requirements

Requires python3 (declared in dependencies): if Noctalia's writeFile cannot open the /dev/hidrawN node O_WRONLY, the plugin falls back to a python3 one-liner that performs the raw binary write. Systems without Python present cannot apply the colour when the direct write path fails. The plugin also needs read/write access to the DualShock 4 /dev/hidrawN node.

On most modern Linux desktops with systemd-logind and uaccess, the device node is automatically accessible when you are logged in at the seat.

On headless systems or systems without uaccess, create a udev rule:

SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="05c4|09cc|0ba0", MODE="0660", GROUP="plugdev"

Then add your user to the plugdev group:

sudo usermod -a -G plugdev $USER

Re-login or run udevadm trigger for the change to take effect.

Usage

Left-click the DS4 Color bar button to instantly apply the last saved colour to every connected DualShock 4. Right-click the bar button (or run the command below) to open the panel:

noctalia msg panel-toggle hy4ri/ds4-color:panel

In the panel: pick a colour with the native picker, type a hex value (RRGGBB / #RRGGBB / 0xRRGGBB), or tap a preset. Save persists the chosen colour (so left-click reapplies it later) without touching the controller; Apply sets the lightbar immediately and also saves it. The saved colour survives restarts.

Settings

Setting Type Default Description
glyph glyph device-gamepad-2 Icon shown on the bar button.

IPC

noctalia msg plugin hy4ri/ds4-color:service all apply <hex>
noctalia msg plugin hy4ri/ds4-color:service all save <hex>

Notes

  • Implements the DualShock 4 HID output report protocol directly in Lua (derived from the Linux kernel drivers/hid/hid-playstation.c): USB report id 0x05 (32 bytes) and Bluetooth report id 0x11 (78 bytes) with the required CRC32 checksum. No kernel drivers or external binaries.
  • Detects all connected DS4 controllers (USB 0x03 and Bluetooth 0x05 bus) by scanning /sys/class/hidraw/*/device/uevent for Sony VID 054c and product IDs 05c4 / 09cc / 0ba0.
  • The last chosen colour is persisted to the plugin data directory (last.json) and restored on next open. Left-clicking the bar button applies the saved colour; the panel's Save button updates it without writing to the controller.
  • No network access. Only the locally detected DualShock 4 controllers are touched.