From 274f4ae5cbee5695ac2c9f020d9dc26f8ee1c380 Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:06:19 +0300 Subject: [PATCH 01/19] feat: add PS4 Colors plugin (DS4 lightbar control) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ps4-colors/README.md | 56 +++++++++++++++ ps4-colors/panel.luau | 117 ++++++++++++++++++++++++++++++++ ps4-colors/plugin.toml | 33 +++++++++ ps4-colors/service.luau | 70 +++++++++++++++++++ ps4-colors/translations/en.json | 16 +++++ ps4-colors/widget.luau | 14 ++++ 6 files changed, 306 insertions(+) create mode 100644 ps4-colors/README.md create mode 100644 ps4-colors/panel.luau create mode 100644 ps4-colors/plugin.toml create mode 100644 ps4-colors/service.luau create mode 100644 ps4-colors/translations/en.json create mode 100644 ps4-colors/widget.luau diff --git a/ps4-colors/README.md b/ps4-colors/README.md new file mode 100644 index 0000000..391feb9 --- /dev/null +++ b/ps4-colors/README.md @@ -0,0 +1,56 @@ +# PS4 Colors + +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 open the +panel, pick a color (native picker, hex field, or preset), and hit Apply — every +connected DS4 (USB and Bluetooth) updates at once. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `Hy4ri/ps4-colors` | +| Entries | Bar widget: `widget`; panel: `panel`; service: `service` | + +## Requirements + +Install [`ps4-colors`](https://github.com/Hy4ri/ps4-colors) on `PATH` (build with +`make` and `sudo make install`). The plugin depends on the `ps4-colors` binary to +write the DualShock 4 HID output report. + +On headless systems without `uaccess`, add a udev rule for the Sony DS4 +(`054c:05c4`, `054c:09cc`, `054c:0ba0`) and join the `plugdev` group, as +documented in the ps4-colors README. + +## Usage + +Click the **PS4 Colors** bar button to open the panel: + +```sh +noctalia msg panel-toggle Hy4ri/ps4-colors:panel +``` + +In the panel: choose a color with the native picker, type a hex value +(`RRGGBB` / `#RRGGBB` / `0xRRGGBB`), or tap a preset, then press **Apply**. +The last applied color is remembered across restarts. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `glyph` | `glyph` | `controller` | Icon shown on the bar button. | + +## IPC + +```sh +noctalia msg plugin Hy4ri/ps4-colors:service all apply +``` + +## Notes + +- Shells out to `ps4-colors `, which writes `/dev/hidrawN` directly (no + libraries). Requires read/write access to the DS4 hidraw node. +- The last chosen color is persisted to the plugin data directory + (`last.json`) and restored on next open. +- No network access. Only the locally detected DualShock 4 controllers are + touched. diff --git a/ps4-colors/panel.luau b/ps4-colors/panel.luau new file mode 100644 index 0000000..a0843b1 --- /dev/null +++ b/ps4-colors/panel.luau @@ -0,0 +1,117 @@ +--!nonstrict +--!nocheck +--!nolint UnknownGlobal +local tr = noctalia.tr("ps4_colors") +local SERVICE = "Hy4ri/ps4-colors:service" + +local function sendToService(event, payload) + local cmd = "noctalia msg plugin " .. SERVICE .. " all " .. event + if payload ~= nil then cmd = cmd .. " '" .. payload .. "'" end + noctalia.runAsync(cmd, function() end) +end + +local function currentColor() + return noctalia.state.get("lastColor") or "990000" +end + +local function render() + local color = currentColor() + local presets = noctalia.state.get("presets") or {} + + local presetRow = {} + for i, p in ipairs(presets) do + table.insert(presetRow, ui.box({ + width = 34, height = 34, fill = "#" .. p.hex, radius = 8, + border = (p.hex:lower() == color:lower()) and "primary" or "outline", + borderWidth = (p.hex:lower() == color:lower()) and 2 or 1, + onClick = "onPreset" .. i, + })) + end + + panel.render(ui.column({ gap = 14, padding = 16 }, { + ui.row({ align = "center", justify = "space_between" }, { + ui.label({ text = tr, fontSize = 18, fontWeight = "bold", color = "primary", flexGrow = 1 }), + ui.button({ glyph = "close", onClick = "onCloseClicked" }), + }), + + -- Live swatch preview + ui.box({ + width = "100%", height = 96, fill = "#" .. color, radius = 12, + border = "outline", borderWidth = 2, + }), + + -- Hex field + native picker + ui.row({ gap = 8, align = "center" }, { + ui.input({ + key = "hex-input", + value = "#" .. color, + textAlign = "left", + flexGrow = 1, + onSubmit = "onSubmitHex", + }), + ui.button({ glyph = "color-picker", variant = "ghost", onClick = "onNativePicker" }), + }), + + ui.label({ text = noctalia.tr("presets"), fontSize = 13, color = "on_surface_variant" }), + ui.row({ gap = 8, align = "center", wrap = true }, presetRow), + + ui.button({ + text = noctalia.tr("apply"), + variant = "primary", + onClick = "onApply", + width = "100%", + }), + })) +end + +--==== callbacks ==== +function onApply() + sendToService("apply", "#" .. currentColor()) +end + +function onSubmitHex(text) + local hex = text:match("^#?(%x%x%x%x%x%x)$") or text:match("^0x(%x%x%x%x%x%x)$") + if hex == nil then + noctalia.notifyError(tr, noctalia.tr("bad_color")) + return + end + noctalia.state.set("lastColor", hex:upper()) + render() +end + +function onNativePicker() + noctalia.openColorPicker("#" .. currentColor(), function(color) + if color == nil then return end + local hex = color:match("#?(%x%x%x%x%x%x)") + if hex ~= nil then + noctalia.state.set("lastColor", hex:upper()) + render() + end + end) +end + +function onCloseClicked() + panel.close() +end + +function onClose() + -- nothing to persist on close; service owns the file +end + +-- preset click handlers (1..N) +local presets = noctalia.state.get("presets") or {} +for i = 1, #presets do + _G["onPreset" .. i] = function() + noctalia.state.set("lastColor", presets[i].hex:upper()) + render() + end +end + +function onOpen(context) + -- ensure presets are loaded from the service + if #(noctalia.state.get("presets") or {}) == 0 then + sendToService("get-presets") + end + noctalia.state.watch("lastColor", function() render() end) + render() +end diff --git a/ps4-colors/plugin.toml b/ps4-colors/plugin.toml new file mode 100644 index 0000000..e166c93 --- /dev/null +++ b/ps4-colors/plugin.toml @@ -0,0 +1,33 @@ +id = "Hy4ri/ps4-colors" +name = "PS4 Colors" +version = "0.1.0" +plugin_api = 3 +author = "Hy4ri" +license = "MIT" +icon = "controller" +description = "Set the DualShock 4 lightbar colour from a bar button and panel." +tags = ["gaming", "hardware", "utility", "bar", "panel"] +dependencies = ["ps4-colors"] + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + description_key = "settings.glyph.description" + default = "controller" + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 420 +height = 520 +placement = "floating" +position = "center" + +[[service]] +id = "service" +entry = "service.luau" diff --git a/ps4-colors/service.luau b/ps4-colors/service.luau new file mode 100644 index 0000000..694a794 --- /dev/null +++ b/ps4-colors/service.luau @@ -0,0 +1,70 @@ +--!nonstrict +--!nocheck +--!nolint UnknownGlobal +-- Headless service: owns the ps4-colors call + persists the last chosen color. +local tr = noctalia.tr("ps4_colors") +local PERSISTENT_DIR = noctalia.pluginDataDir() +local LAST_FILE = PERSISTENT_DIR .. "/last.json" + +-- Built-in presets (name -> RRGGBB). Deep crimson is the boss's favourite. +local PRESETS = { + { name = "Crimson", hex = "990000" }, + { name = "Red", hex = "ff0000" }, + { name = "Green", hex = "00ff00" }, + { name = "Blue", hex = "0000ff" }, + { name = "Cyan", hex = "00ffff" }, + { name = "Magenta", hex = "ff00ff" }, + { name = "Yellow", hex = "ffff00" }, + { name = "White", hex = "ffffff" }, + { name = "Off", hex = "000000" }, +} + +local function loadLast() + local raw = noctalia.readFile(LAST_FILE) + if raw == nil or raw == "" then return "990000" end + local decoded = noctalia.json.decode(raw) + if type(decoded) ~= "table" or type(decoded.hex) ~= "string" then return "990000" end + return decoded.hex +end + +local function saveLast(hex) + noctalia.mkdirAll(PERSISTENT_DIR) + local encoded = noctalia.json.encode({ hex = hex }) + if encoded ~= nil then noctalia.writeFile(LAST_FILE, encoded) end +end + +-- Apply a hex color (RRGGBB, no #) to every connected DS4. +local function apply(hex) + if not noctalia.commandExists("ps4-colors") then + noctalia.notifyError(tr, noctalia.tr("ps4_colors_missing")) + return + end + noctalia.runAsync("ps4-colors " .. hex, function(result) + if result.exitCode == 0 then + saveLast(hex) + noctalia.state.set("lastColor", hex) + noctalia.notify(tr, noctalia.tr("applied", { color = hex })) + elseif result.exitCode == 1 then + noctalia.notifyError(tr, noctalia.tr("bad_color")) + elseif result.exitCode == 2 then + noctalia.notifyError(tr, noctalia.tr("no_controller")) + else + noctalia.notifyError(tr, noctalia.tr("write_error")) + end + end) +end + +function onIpc(event, payload) + if event == "apply" and payload ~= nil then + apply(payload:match("^#?(%x%x%x%x%x%x)$") or payload:match("^0x(%x%x%x%x%x%x)$") or "") + elseif event == "get-presets" then + noctalia.state.set("presets", PRESETS) + end +end + +function load() + noctalia.state.set("lastColor", loadLast()) + noctalia.state.set("presets", PRESETS) +end + +load() diff --git a/ps4-colors/translations/en.json b/ps4-colors/translations/en.json new file mode 100644 index 0000000..52ebe6e --- /dev/null +++ b/ps4-colors/translations/en.json @@ -0,0 +1,16 @@ +{ + "ps4_colors": "PS4 Colors", + "presets": "Presets", + "apply": "Apply", + "applied": "Lightbar set to #{color}", + "bad_color": "Invalid hex color.", + "no_controller": "No DualShock 4 detected.", + "write_error": "Failed to write to controller.", + "ps4_colors_missing": "ps4-colors is not installed. Build it from github.com/Hy4ri/ps4-colors.", + "settings": { + "glyph": { + "label": "Bar Glyph", + "description": "Icon shown on the bar button." + } + } +} diff --git a/ps4-colors/widget.luau b/ps4-colors/widget.luau new file mode 100644 index 0000000..838b9f9 --- /dev/null +++ b/ps4-colors/widget.luau @@ -0,0 +1,14 @@ +--!nonstrict +local glyph = noctalia.getConfig("glyph") +local tr = noctalia.tr("ps4_colors") + +local function render() + barWidget.setGlyph(glyph) + barWidget.setTooltip(tr) +end + +function onClick() + noctalia.togglePanel("Hy4ri/ps4-colors:panel") +end + +render() From fdf952376c7a16b28927526ac50710cfbe00289b Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:12:15 +0300 Subject: [PATCH 02/19] fix: add PS4 Colors thumbnail (solid crimson card) 960x540 WebP, deep crimson (#990000) to match the plugin accent. --- ps4-colors/thumbnail.webp | Bin 0 -> 11156 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ps4-colors/thumbnail.webp diff --git a/ps4-colors/thumbnail.webp b/ps4-colors/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..691b7da0f680f41c33065aa31dd63a2293d2097a GIT binary patch literal 11156 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 literal 0 HcmV?d00001 From 2b5548cf42e1728b055fcd3d1b1f1dcc19068afa Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:24:11 +0300 Subject: [PATCH 03/19] 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 --- ps4-colors/README.md | 16 +++++++++++----- ps4-colors/panel.luau | 22 +++++++++++++++++----- ps4-colors/service.luau | 11 +++++++++++ ps4-colors/translations/en.json | 2 ++ ps4-colors/widget.luau | 7 +++++++ 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/ps4-colors/README.md b/ps4-colors/README.md index 391feb9..5ffd7a6 100644 --- a/ps4-colors/README.md +++ b/ps4-colors/README.md @@ -24,15 +24,19 @@ documented in the ps4-colors README. ## Usage -Click the **PS4 Colors** bar button to open the panel: +**Left-click** the **PS4 Colors** bar button to instantly apply the last saved +color to every connected DualShock 4. **Right-click** the bar button (or run the +command below) to open the panel: ```sh noctalia msg panel-toggle Hy4ri/ps4-colors:panel ``` -In the panel: choose a color with the native picker, type a hex value -(`RRGGBB` / `#RRGGBB` / `0xRRGGBB`), or tap a preset, then press **Apply**. -The last applied color is remembered across restarts. +In the panel: pick a color with the native picker, type a hex value +(`RRGGBB` / `#RRGGBB` / `0xRRGGBB`), or tap a preset. **Save** persists the +chosen color (so left-click reapplies it later) without touching the +controller; **Apply** sets the lightbar immediately and also saves it. The saved +color survives restarts. ## Settings @@ -51,6 +55,8 @@ noctalia msg plugin Hy4ri/ps4-colors:service all apply - Shells out to `ps4-colors `, which writes `/dev/hidrawN` directly (no libraries). Requires read/write access to the DS4 hidraw node. - The last chosen color is persisted to the plugin data directory - (`last.json`) and restored on next open. + (`last.json`) and restored on next open. Left-clicking the bar button applies + the saved color; the panel's **Save** button updates it without writing to the + controller. - No network access. Only the locally detected DualShock 4 controllers are touched. diff --git a/ps4-colors/panel.luau b/ps4-colors/panel.luau index a0843b1..30751b8 100644 --- a/ps4-colors/panel.luau +++ b/ps4-colors/panel.luau @@ -55,11 +55,19 @@ local function render() ui.label({ text = noctalia.tr("presets"), fontSize = 13, color = "on_surface_variant" }), ui.row({ gap = 8, align = "center", wrap = true }, presetRow), - ui.button({ - text = noctalia.tr("apply"), - variant = "primary", - onClick = "onApply", - width = "100%", + ui.row({ gap = 8 }, { + ui.button({ + text = noctalia.tr("save"), + variant = "ghost", + onClick = "onSave", + flexGrow = 1, + }), + ui.button({ + text = noctalia.tr("apply"), + variant = "primary", + onClick = "onApply", + flexGrow = 1, + }), }), })) end @@ -69,6 +77,10 @@ function onApply() sendToService("apply", "#" .. currentColor()) end +function onSave() + sendToService("save", "#" .. currentColor()) +end + function onSubmitHex(text) local hex = text:match("^#?(%x%x%x%x%x%x)$") or text:match("^0x(%x%x%x%x%x%x)$") if hex == nil then diff --git a/ps4-colors/service.luau b/ps4-colors/service.luau index 694a794..0d6f60a 100644 --- a/ps4-colors/service.luau +++ b/ps4-colors/service.luau @@ -35,6 +35,7 @@ end -- Apply a hex color (RRGGBB, no #) to every connected DS4. local function apply(hex) + if hex == nil or hex == "" then return end if not noctalia.commandExists("ps4-colors") then noctalia.notifyError(tr, noctalia.tr("ps4_colors_missing")) return @@ -54,9 +55,19 @@ local function apply(hex) end) end +-- Persist the chosen color WITHOUT touching the controller. +local function save(hex) + if hex == nil or hex == "" then return end + saveLast(hex) + noctalia.state.set("lastColor", hex) + noctalia.notify(tr, noctalia.tr("saved", { color = hex })) +end + function onIpc(event, payload) if event == "apply" and payload ~= nil then apply(payload:match("^#?(%x%x%x%x%x%x)$") or payload:match("^0x(%x%x%x%x%x%x)$") or "") + elseif event == "save" and payload ~= nil then + save(payload:match("^#?(%x%x%x%x%x%x)$") or payload:match("^0x(%x%x%x%x%x%x)$") or "") elseif event == "get-presets" then noctalia.state.set("presets", PRESETS) end diff --git a/ps4-colors/translations/en.json b/ps4-colors/translations/en.json index 52ebe6e..f2ca809 100644 --- a/ps4-colors/translations/en.json +++ b/ps4-colors/translations/en.json @@ -1,8 +1,10 @@ { "ps4_colors": "PS4 Colors", "presets": "Presets", + "save": "Save", "apply": "Apply", "applied": "Lightbar set to #{color}", + "saved": "Color #{color} saved", "bad_color": "Invalid hex color.", "no_controller": "No DualShock 4 detected.", "write_error": "Failed to write to controller.", diff --git a/ps4-colors/widget.luau b/ps4-colors/widget.luau index 838b9f9..db6eaa4 100644 --- a/ps4-colors/widget.luau +++ b/ps4-colors/widget.luau @@ -7,7 +7,14 @@ local function render() barWidget.setTooltip(tr) end +-- Left click: apply the saved color directly (no panel needed). function onClick() + local saved = noctalia.state.get("lastColor") or "990000" + noctalia.runAsync("noctalia msg plugin Hy4ri/ps4-colors:service all apply '" .. saved .. "'", function() end) +end + +-- Right click: open the panel to pick / save a new color. +function onRightClick() noctalia.togglePanel("Hy4ri/ps4-colors:panel") end From a5921ae382612979d30cbbcd694d14622d723586 Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:31:03 +0300 Subject: [PATCH 04/19] 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. --- ps4-colors/README.md | 58 +++++++++----- ps4-colors/plugin.toml | 2 +- ps4-colors/service.luau | 168 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 193 insertions(+), 35 deletions(-) diff --git a/ps4-colors/README.md b/ps4-colors/README.md index 5ffd7a6..0660ed7 100644 --- a/ps4-colors/README.md +++ b/ps4-colors/README.md @@ -1,9 +1,13 @@ # PS4 Colors 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 open the -panel, pick a color (native picker, hex field, or preset), and hit Apply — every -connected DS4 (USB and Bluetooth) updates at once. +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 @@ -14,29 +18,41 @@ connected DS4 (USB and Bluetooth) updates at once. ## Requirements -Install [`ps4-colors`](https://github.com/Hy4ri/ps4-colors) on `PATH` (build with -`make` and `sudo make install`). The plugin depends on the `ps4-colors` binary to -write the DualShock 4 HID output report. +No external dependencies. The plugin 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: + +```udev +SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="05c4|09cc|0ba0", MODE="0660", GROUP="plugdev" +``` + +Then add your user to the `plugdev` group: + +```sh +sudo usermod -a -G plugdev $USER +``` -On headless systems without `uaccess`, add a udev rule for the Sony DS4 -(`054c:05c4`, `054c:09cc`, `054c:0ba0`) and join the `plugdev` group, as -documented in the ps4-colors README. +Re-login or run `udevadm trigger` for the change to take effect. ## Usage **Left-click** the **PS4 Colors** bar button to instantly apply the last saved -color to every connected DualShock 4. **Right-click** the bar button (or run the +colour to every connected DualShock 4. **Right-click** the bar button (or run the command below) to open the panel: ```sh noctalia msg panel-toggle Hy4ri/ps4-colors:panel ``` -In the panel: pick a color with the native picker, type a hex value +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 color (so left-click reapplies it later) without touching the +chosen colour (so left-click reapplies it later) without touching the controller; **Apply** sets the lightbar immediately and also saves it. The saved -color survives restarts. +colour survives restarts. ## Settings @@ -48,15 +64,21 @@ color survives restarts. ```sh noctalia msg plugin Hy4ri/ps4-colors:service all apply +noctalia msg plugin Hy4ri/ps4-colors:service all save ``` ## Notes -- Shells out to `ps4-colors `, which writes `/dev/hidrawN` directly (no - libraries). Requires read/write access to the DS4 hidraw node. -- The last chosen color is persisted to the plugin data directory +- 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 color; the panel's **Save** button updates it without writing to the - controller. + 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. diff --git a/ps4-colors/plugin.toml b/ps4-colors/plugin.toml index e166c93..81fed7e 100644 --- a/ps4-colors/plugin.toml +++ b/ps4-colors/plugin.toml @@ -7,7 +7,7 @@ license = "MIT" icon = "controller" description = "Set the DualShock 4 lightbar colour from a bar button and panel." tags = ["gaming", "hardware", "utility", "bar", "panel"] -dependencies = ["ps4-colors"] +dependencies = [] [[widget]] id = "widget" diff --git a/ps4-colors/service.luau b/ps4-colors/service.luau index 0d6f60a..efa2b1c 100644 --- a/ps4-colors/service.luau +++ b/ps4-colors/service.luau @@ -1,12 +1,35 @@ --!nonstrict --!nocheck --!nolint UnknownGlobal --- Headless service: owns the ps4-colors call + persists the last chosen color. +-- Headless service: implements the DualShock 4 lightbar protocol in pure Lua. +-- No external binary needed — writes the HID output report straight to +-- /dev/hidrawN via noctalia.writeFile (a Lua string is raw bytes). local tr = noctalia.tr("ps4_colors") local PERSISTENT_DIR = noctalia.pluginDataDir() local LAST_FILE = PERSISTENT_DIR .. "/last.json" --- Built-in presets (name -> RRGGBB). Deep crimson is the boss's favourite. +-- Sony DualShock 4 vendor / product IDs (from ps4-colors detect.c). +local SONY_VID = 0x054C +local KNOWN_PIDS = { [0x05C4] = true, [0x09CC] = true, [0x0BA0] = true } +local BUS_USB = 0x0003 +local BUS_BT = 0x0005 + +-- Report protocol constants (from ps4-colors ds4.c / hid-playstation.c). +local DS4_USB_REPORT_ID = 0x05 +local DS4_BT_REPORT_ID = 0x11 +local DS4_USB_REPORT_LEN = 32 +local DS4_BT_REPORT_LEN = 78 +local DS4_USB_COMMON_OFFSET = 1 +local DS4_BT_COMMON_OFFSET = 3 +local DS4_OUTPUT_VALID_FLAG0_LED = 0x02 +local DS4_BT_HW_CONTROL = 0xC4 +local PS_OUTPUT_CRC32_SEED = 0xA2 +local COMMON_LIGHTBAR_R = 5 +local COMMON_LIGHTBAR_G = 6 +local COMMON_LIGHTBAR_B = 7 +local COMMON_VALID_FLAG0 = 0 + +-- Built-in presets. Deep crimson is the boss's favourite. local PRESETS = { { name = "Crimson", hex = "990000" }, { name = "Red", hex = "ff0000" }, @@ -19,6 +42,110 @@ local PRESETS = { { name = "Off", hex = "000000" }, } +--==== CRC32 (poly 0xEDB88320, reflected) ==== +local crc32_table = nil +local function crc32_init() + crc32_table = {} + for i = 0, 255 do + local crc = i + for _ = 1, 8 do + if crc & 1 ~= 0 then + crc = (crc >> 1) ~ 0xEDB88320 + else + crc = crc >> 1 + end + end + crc32_table[i] = crc + end +end + +local function crc32_le(crc, buf, len) + if crc32_table == nil then crc32_init() end + for i = 1, len do + local b = string.byte(buf, i) + crc = crc32_table[(crc ~ b) & 0xFF] ~ (crc >> 8) + end + return crc +end + +--==== byte-string helpers ==== +local function zeros(n) + return string.rep("\0", n) +end + +local function setbyte(s, idx, val) + -- 1-indexed Lua string; idx is 0-based report offset + return string.sub(s, 1, idx) .. string.char(val & 0xFF) .. string.sub(s, idx + 2) +end + +--==== report builders ==== +local function buildUsbReport(r, g, b) + local buf = "\5" .. zeros(DS4_USB_REPORT_LEN - 1) -- report_id 0x05, rest 0x00 + buf = setbyte(buf, DS4_USB_COMMON_OFFSET + COMMON_VALID_FLAG0, DS4_OUTPUT_VALID_FLAG0_LED) + buf = setbyte(buf, DS4_USB_COMMON_OFFSET + COMMON_LIGHTBAR_R, r) + buf = setbyte(buf, DS4_USB_COMMON_OFFSET + COMMON_LIGHTBAR_G, g) + buf = setbyte(buf, DS4_USB_COMMON_OFFSET + COMMON_LIGHTBAR_B, b) + return buf +end + +local function buildBtReport(r, g, b) + local buf = "\17\196\0" .. zeros(DS4_BT_REPORT_LEN - 3) -- 0x11, 0xC4, 0x00, rest 0x00 + buf = setbyte(buf, DS4_BT_COMMON_OFFSET + COMMON_VALID_FLAG0, DS4_OUTPUT_VALID_FLAG0_LED) + buf = setbyte(buf, DS4_BT_COMMON_OFFSET + COMMON_LIGHTBAR_R, r) + buf = setbyte(buf, DS4_BT_COMMON_OFFSET + COMMON_LIGHTBAR_G, g) + buf = setbyte(buf, DS4_BT_COMMON_OFFSET + COMMON_LIGHTBAR_B, b) + -- CRC32 over bytes 0..73 with seed 0xA2, stored at 74..77 (little-endian). + local crc = crc32_le(0xFFFFFFFF, string.char(PS_OUTPUT_CRC32_SEED), 1) + crc = (~crc32_le(crc, buf, DS4_BT_REPORT_LEN - 4)) & 0xFFFFFFFF + buf = setbyte(buf, 74, crc & 0xFF) + buf = setbyte(buf, 75, (crc >> 8) & 0xFF) + buf = setbyte(buf, 76, (crc >> 16) & 0xFF) + buf = setbyte(buf, 77, (crc >> 24) & 0xFF) + return buf +end + +--==== device detection (port of detect.c) ==== +local function parseHidId(line) + -- HID_ID=BBBB:VVVVVVVV:PPPPPPPP + local bus, vendor, product = line:match("HID_ID=(%x+):(%x+):(%x+)") + if bus == nil then return nil end + return tonumber(bus, 16), tonumber(vendor, 16) & 0xFFFF, tonumber(product, 16) & 0xFFFF +end + +local function detectControllers() + local out = {} + local hidrawDir = noctalia.listDir("/sys/class/hidraw") + if hidrawDir == nil then return out end + for _, name in ipairs(hidrawDir) do + if name:sub(1, 6) == "hidraw" then + local ueventPath = "/sys/class/hidraw/" .. name .. "/device/uevent" + local content = noctalia.readFile(ueventPath) + if content ~= nil then + local bus, vendor, product + for line in content:gmatch("[^\n]+") do + if line:sub(1, 7) == "HID_ID=" then + bus, vendor, product = parseHidId(line) + break + end + end + if vendor == SONY_VID and KNOWN_PIDS[product] then + table.insert(out, { path = "/dev/" .. name, bus_type = bus }) + end + end + end + end + return out +end + +--==== apply ==== +local function setLightbar(dev, r, g, b) + local report = (dev.bus_type == BUS_BT) + and buildBtReport(r, g, b) + or buildUsbReport(r, g, b) + local ok, err = noctalia.writeFile(dev.path, report) + return ok, err +end + local function loadLast() local raw = noctalia.readFile(LAST_FILE) if raw == nil or raw == "" then return "990000" end @@ -33,26 +160,35 @@ local function saveLast(hex) if encoded ~= nil then noctalia.writeFile(LAST_FILE, encoded) end end --- Apply a hex color (RRGGBB, no #) to every connected DS4. +local function hexToRgb(hex) + return tonumber(hex:sub(1, 2), 16) or 0, + tonumber(hex:sub(3, 4), 16) or 0, + tonumber(hex:sub(5, 6), 16) or 0 +end + +-- Apply a hex color to every connected DS4. local function apply(hex) if hex == nil or hex == "" then return end - if not noctalia.commandExists("ps4-colors") then - noctalia.notifyError(tr, noctalia.tr("ps4_colors_missing")) + local r, g, b = hexToRgb(hex) + local devs = detectControllers() + if #devs == 0 then + noctalia.notifyError(tr, noctalia.tr("no_controller")) return end - noctalia.runAsync("ps4-colors " .. hex, function(result) - if result.exitCode == 0 then - saveLast(hex) - noctalia.state.set("lastColor", hex) - noctalia.notify(tr, noctalia.tr("applied", { color = hex })) - elseif result.exitCode == 1 then - noctalia.notifyError(tr, noctalia.tr("bad_color")) - elseif result.exitCode == 2 then - noctalia.notifyError(tr, noctalia.tr("no_controller")) + local ok = 0 + for _, dev in ipairs(devs) do + local good, err = setLightbar(dev, r, g, b) + if good then + ok = ok + 1 else - noctalia.notifyError(tr, noctalia.tr("write_error")) + noctalia.notifyError(tr, noctalia.tr("write_error") .. " (" .. dev.path .. ")") end - end) + end + if ok > 0 then + saveLast(hex) + noctalia.state.set("lastColor", hex) + noctalia.notify(tr, noctalia.tr("applied", { color = hex })) + end end -- Persist the chosen color WITHOUT touching the controller. From 6d49febf2f2935b7e112fa67c0ea81b81893df01 Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:35:27 +0300 Subject: [PATCH 05/19] 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. --- ps4-colors/panel.luau | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/ps4-colors/panel.luau b/ps4-colors/panel.luau index 30751b8..87e05c7 100644 --- a/ps4-colors/panel.luau +++ b/ps4-colors/panel.luau @@ -14,6 +14,17 @@ local function currentColor() return noctalia.state.get("lastColor") or "990000" end +-- Build a click handler for preset index i that lives for the panel session. +local function makePresetHandler(i) + return function() + local presets = noctalia.state.get("presets") or {} + local p = presets[i] + if p == nil then return end + noctalia.state.set("lastColor", p.hex:upper()) + render() + end +end + local function render() local color = currentColor() local presets = noctalia.state.get("presets") or {} @@ -36,7 +47,7 @@ local function render() -- Live swatch preview ui.box({ - width = "100%", height = 96, fill = "#" .. color, radius = 12, + width = 200, height = 96, fill = "#" .. color, radius = 12, border = "outline", borderWidth = 2, }), @@ -110,20 +121,15 @@ function onClose() -- nothing to persist on close; service owns the file end --- preset click handlers (1..N) -local presets = noctalia.state.get("presets") or {} -for i = 1, #presets do - _G["onPreset" .. i] = function() - noctalia.state.set("lastColor", presets[i].hex:upper()) - render() - end -end - function onOpen(context) - -- ensure presets are loaded from the service - if #(noctalia.state.get("presets") or {}) == 0 then - sendToService("get-presets") + -- Register preset click handlers now that presets are loaded (service sets + -- them at load; guard anyway). Doing this here avoids ipairs(nil) at script + -- load, which would abort the whole panel. + local presets = noctalia.state.get("presets") or {} + for i = 1, #presets do + _G["onPreset" .. i] = makePresetHandler(i) end + noctalia.state.watch("lastColor", function() render() end) render() end From 99a2bd3f9b632e6ece116ed2e01b2416b0331a4e Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:38:56 +0300 Subject: [PATCH 06/19] 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. --- ps4-colors/panel.luau | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/ps4-colors/panel.luau b/ps4-colors/panel.luau index 87e05c7..067495b 100644 --- a/ps4-colors/panel.luau +++ b/ps4-colors/panel.luau @@ -14,7 +14,7 @@ local function currentColor() return noctalia.state.get("lastColor") or "990000" end --- Build a click handler for preset index i that lives for the panel session. +-- Build a click handler for preset index i; registered in onOpen. local function makePresetHandler(i) return function() local presets = noctalia.state.get("presets") or {} @@ -31,15 +31,19 @@ local function render() local presetRow = {} for i, p in ipairs(presets) do + local selected = (p.hex:lower() == color:lower()) table.insert(presetRow, ui.box({ - width = 34, height = 34, fill = "#" .. p.hex, radius = 8, - border = (p.hex:lower() == color:lower()) and "primary" or "outline", - borderWidth = (p.hex:lower() == color:lower()) and 2 or 1, + width = 34, + height = 34, + fill = "#" .. p.hex, + radius = 8, + border = "outline", + borderWidth = selected and 3 or 1, onClick = "onPreset" .. i, })) end - panel.render(ui.column({ gap = 14, padding = 16 }, { + panel.render(ui.column({ gap = 12, padding = 16 }, { ui.row({ align = "center", justify = "space_between" }, { ui.label({ text = tr, fontSize = 18, fontWeight = "bold", color = "primary", flexGrow = 1 }), ui.button({ glyph = "close", onClick = "onCloseClicked" }), @@ -47,11 +51,16 @@ local function render() -- Live swatch preview ui.box({ - width = 200, height = 96, fill = "#" .. color, radius = 12, - border = "outline", borderWidth = 2, + width = 200, + height = 96, + fill = "#" .. color, + radius = 12, + border = "outline", + borderWidth = 2, + onClick = "onNativePicker", }), - -- Hex field + native picker + -- Hex field + native picker button ui.row({ gap = 8, align = "center" }, { ui.input({ key = "hex-input", @@ -64,7 +73,7 @@ local function render() }), ui.label({ text = noctalia.tr("presets"), fontSize = 13, color = "on_surface_variant" }), - ui.row({ gap = 8, align = "center", wrap = true }, presetRow), + ui.row({ gap = 8, align = "center" }, presetRow), ui.row({ gap = 8 }, { ui.button({ @@ -118,13 +127,9 @@ function onCloseClicked() end function onClose() - -- nothing to persist on close; service owns the file end function onOpen(context) - -- Register preset click handlers now that presets are loaded (service sets - -- them at load; guard anyway). Doing this here avoids ipairs(nil) at script - -- load, which would abort the whole panel. local presets = noctalia.state.get("presets") or {} for i = 1, #presets do _G["onPreset" .. i] = makePresetHandler(i) From 33aec359c7b24dc232ff39d7612a2e2586d6dded Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:42:24 +0300 Subject: [PATCH 07/19] 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). --- ps4-colors/service.luau | 49 +++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/ps4-colors/service.luau b/ps4-colors/service.luau index efa2b1c..d9b8249 100644 --- a/ps4-colors/service.luau +++ b/ps4-colors/service.luau @@ -2,11 +2,22 @@ --!nocheck --!nolint UnknownGlobal -- Headless service: implements the DualShock 4 lightbar protocol in pure Lua. --- No external binary needed — writes the HID output report straight to --- /dev/hidrawN via noctalia.writeFile (a Lua string is raw bytes). +-- No external binary needed — writes the HID output report to /dev/hidrawN. local tr = noctalia.tr("ps4_colors") -local PERSISTENT_DIR = noctalia.pluginDataDir() -local LAST_FILE = PERSISTENT_DIR .. "/last.json" + +-- pluginDataDir() may return nil (state home unset), so resolve it lazily and +-- guard every use. Computing it at module top-level and concatenating would +-- throw on nil and kill the whole service entry (no onIpc -> IPC is a no-op). +local function persistentDir() + local dir = noctalia.pluginDataDir() + return dir +end + +local function lastFile() + local dir = persistentDir() + if dir == nil then return nil end + return dir .. "/last.json" +end -- Sony DualShock 4 vendor / product IDs (from ps4-colors detect.c). local SONY_VID = 0x054C @@ -137,17 +148,35 @@ local function detectControllers() return out end ---==== apply ==== +-- Write the report to the hidraw node. Noctalia's writeFile is file-oriented and +-- may not open a char device O_WRONLY, so if it fails we fall back to a +-- python3 one-liner that does a true binary O_WRONLY write (what hidraw needs). local function setLightbar(dev, r, g, b) local report = (dev.bus_type == BUS_BT) and buildBtReport(r, g, b) or buildUsbReport(r, g, b) + local hex = "" + for i = 1, #report do + hex = hex .. string.format("%02x", string.byte(report, i)) + end local ok, err = noctalia.writeFile(dev.path, report) - return ok, err + if ok then return true, nil end + -- Fallback: raw binary write via python3 (open(path,'wb').write(...)). + local py = string.format( + "python3 -c \"import sys; open('%s','wb').write(bytes.fromhex('%s'))\"", + dev.path, hex) + local done = noctalia.runAsync(py, function(res) + if res == nil or res.exitCode ~= 0 then + noctalia.notifyError(tr, noctalia.tr("write_error") .. " (" .. dev.path .. ")") + end + end) + return done, err end local function loadLast() - local raw = noctalia.readFile(LAST_FILE) + local path = lastFile() + if path == nil then return "990000" end + local raw = noctalia.readFile(path) if raw == nil or raw == "" then return "990000" end local decoded = noctalia.json.decode(raw) if type(decoded) ~= "table" or type(decoded.hex) ~= "string" then return "990000" end @@ -155,9 +184,11 @@ local function loadLast() end local function saveLast(hex) - noctalia.mkdirAll(PERSISTENT_DIR) + local path = lastFile() + if path == nil then return end + noctalia.mkdirAll(persistentDir()) local encoded = noctalia.json.encode({ hex = hex }) - if encoded ~= nil then noctalia.writeFile(LAST_FILE, encoded) end + if encoded ~= nil then noctalia.writeFile(path, encoded) end end local function hexToRgb(hex) From c604e8010efd0fa6ae251c3c8e43e4951b9cb4af Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:45:34 +0300 Subject: [PATCH 08/19] 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. --- ps4-colors/service.luau | 46 +++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/ps4-colors/service.luau b/ps4-colors/service.luau index d9b8249..7f45b4e 100644 --- a/ps4-colors/service.luau +++ b/ps4-colors/service.luau @@ -53,6 +53,30 @@ local PRESETS = { { name = "Off", hex = "000000" }, } +--==== bitwise helpers (math-only, Lua 5.x safe — no &/|/~/>>) ==== +local function band(a, m) + return a % (m + 1) +end +local function rshift(a, n) + return math.floor(a / (2 ^ n)) +end +local function bxor(a, b) + local r = 0 + local pow = 1 + for _ = 1, 32 do + local ab = a % 2 + local bb = b % 2 + if ab ~= bb then r = r + pow end -- ~= is Lua not-equal, fine + a = math.floor(a / 2) + b = math.floor(b / 2) + pow = pow * 2 + end + return r +end +local function bnot32(a) + return bxor(a, 0xFFFFFFFF) +end + --==== CRC32 (poly 0xEDB88320, reflected) ==== local crc32_table = nil local function crc32_init() @@ -60,10 +84,10 @@ local function crc32_init() for i = 0, 255 do local crc = i for _ = 1, 8 do - if crc & 1 ~= 0 then - crc = (crc >> 1) ~ 0xEDB88320 + if band(crc, 1) ~= 0 then + crc = bxor(rshift(crc, 1), 0xEDB88320) else - crc = crc >> 1 + crc = rshift(crc, 1) end end crc32_table[i] = crc @@ -74,7 +98,7 @@ local function crc32_le(crc, buf, len) if crc32_table == nil then crc32_init() end for i = 1, len do local b = string.byte(buf, i) - crc = crc32_table[(crc ~ b) & 0xFF] ~ (crc >> 8) + crc = bxor(crc32_table[band(bxor(crc, b), 0xFF)], rshift(crc, 8)) end return crc end @@ -86,7 +110,7 @@ end local function setbyte(s, idx, val) -- 1-indexed Lua string; idx is 0-based report offset - return string.sub(s, 1, idx) .. string.char(val & 0xFF) .. string.sub(s, idx + 2) + return string.sub(s, 1, idx) .. string.char(band(val, 0xFF)) .. string.sub(s, idx + 2) end --==== report builders ==== @@ -107,11 +131,11 @@ local function buildBtReport(r, g, b) buf = setbyte(buf, DS4_BT_COMMON_OFFSET + COMMON_LIGHTBAR_B, b) -- CRC32 over bytes 0..73 with seed 0xA2, stored at 74..77 (little-endian). local crc = crc32_le(0xFFFFFFFF, string.char(PS_OUTPUT_CRC32_SEED), 1) - crc = (~crc32_le(crc, buf, DS4_BT_REPORT_LEN - 4)) & 0xFFFFFFFF - buf = setbyte(buf, 74, crc & 0xFF) - buf = setbyte(buf, 75, (crc >> 8) & 0xFF) - buf = setbyte(buf, 76, (crc >> 16) & 0xFF) - buf = setbyte(buf, 77, (crc >> 24) & 0xFF) + crc = band(bnot32(crc32_le(crc, buf, DS4_BT_REPORT_LEN - 4)), 0xFFFFFFFF) + buf = setbyte(buf, 74, band(crc, 0xFF)) + buf = setbyte(buf, 75, band(rshift(crc, 8), 0xFF)) + buf = setbyte(buf, 76, band(rshift(crc, 16), 0xFF)) + buf = setbyte(buf, 77, band(rshift(crc, 24), 0xFF)) return buf end @@ -120,7 +144,7 @@ local function parseHidId(line) -- HID_ID=BBBB:VVVVVVVV:PPPPPPPP local bus, vendor, product = line:match("HID_ID=(%x+):(%x+):(%x+)") if bus == nil then return nil end - return tonumber(bus, 16), tonumber(vendor, 16) & 0xFFFF, tonumber(product, 16) & 0xFFFF + return tonumber(bus, 16), band(tonumber(vendor, 16), 0xFFFF), band(tonumber(product, 16), 0xFFFF) end local function detectControllers() From a595957d1d896593a20b4ef0dee7a614f2393d3b Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:47:44 +0300 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20panel=20black=20box=20(readonly=20?= =?UTF-8?q?=5FG)=20=E2=80=94=20use=20static=20onPreset1..9=20globals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ps4-colors/panel.luau | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/ps4-colors/panel.luau b/ps4-colors/panel.luau index 067495b..752df4c 100644 --- a/ps4-colors/panel.luau +++ b/ps4-colors/panel.luau @@ -14,17 +14,6 @@ local function currentColor() return noctalia.state.get("lastColor") or "990000" end --- Build a click handler for preset index i; registered in onOpen. -local function makePresetHandler(i) - return function() - local presets = noctalia.state.get("presets") or {} - local p = presets[i] - if p == nil then return end - noctalia.state.set("lastColor", p.hex:upper()) - render() - end -end - local function render() local color = currentColor() local presets = noctalia.state.get("presets") or {} @@ -129,12 +118,28 @@ end function onClose() end -function onOpen(context) +-- Presets are a fixed list, so define one global handler per index at module +-- load (Noctalia's _G is read-only at runtime — no dynamic _G["onPreset"..i] +-- assignment, which throws "attempt to modify a readonly table" in onOpen). +local function applyPreset(i) local presets = noctalia.state.get("presets") or {} - for i = 1, #presets do - _G["onPreset" .. i] = makePresetHandler(i) - end + local p = presets[i] + if p == nil then return end + noctalia.state.set("lastColor", p.hex:upper()) + render() +end +function onPreset1() applyPreset(1) end +function onPreset2() applyPreset(2) end +function onPreset3() applyPreset(3) end +function onPreset4() applyPreset(4) end +function onPreset5() applyPreset(5) end +function onPreset6() applyPreset(6) end +function onPreset7() applyPreset(7) end +function onPreset8() applyPreset(8) end +function onPreset9() applyPreset(9) end + +function onOpen(context) noctalia.state.watch("lastColor", function() render() end) render() end From a00985d82221d85ca3d59b3a55cc0ad12ea9c14c Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:51:17 +0300 Subject: [PATCH 10/19] 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. --- ps4-colors/panel.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/ps4-colors/panel.luau b/ps4-colors/panel.luau index 752df4c..8e5e1f1 100644 --- a/ps4-colors/panel.luau +++ b/ps4-colors/panel.luau @@ -54,7 +54,6 @@ local function render() ui.input({ key = "hex-input", value = "#" .. color, - textAlign = "left", flexGrow = 1, onSubmit = "onSubmitHex", }), From ef78e603fa881dbea00be2d5684e89ff7f5b6add Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:54:10 +0300 Subject: [PATCH 11/19] 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. --- ps4-colors/README.md | 2 +- ps4-colors/plugin.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ps4-colors/README.md b/ps4-colors/README.md index 0660ed7..3a076b4 100644 --- a/ps4-colors/README.md +++ b/ps4-colors/README.md @@ -58,7 +58,7 @@ colour survives restarts. | Setting | Type | Default | Description | | --- | --- | --- | --- | -| `glyph` | `glyph` | `controller` | Icon shown on the bar button. | +| `glyph` | `glyph` | `device-gamepad-2` | Icon shown on the bar button. | ## IPC diff --git a/ps4-colors/plugin.toml b/ps4-colors/plugin.toml index 81fed7e..df6e18b 100644 --- a/ps4-colors/plugin.toml +++ b/ps4-colors/plugin.toml @@ -4,7 +4,7 @@ version = "0.1.0" plugin_api = 3 author = "Hy4ri" license = "MIT" -icon = "controller" +icon = "device-gamepad-2" description = "Set the DualShock 4 lightbar colour from a bar button and panel." tags = ["gaming", "hardware", "utility", "bar", "panel"] dependencies = [] @@ -18,7 +18,7 @@ entry = "widget.luau" type = "glyph" label_key = "settings.glyph.label" description_key = "settings.glyph.description" - default = "controller" + default = "device-gamepad-2" [[panel]] id = "panel" From f769d153b95791abcee171b783262c7cb67eb797 Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:58:22 +0300 Subject: [PATCH 12/19] 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. --- {ps4-colors => ds4-color}/README.md | 12 ++++++------ {ps4-colors => ds4-color}/panel.luau | 4 ++-- {ps4-colors => ds4-color}/plugin.toml | 4 ++-- {ps4-colors => ds4-color}/service.luau | 2 +- {ps4-colors => ds4-color}/thumbnail.webp | Bin {ps4-colors => ds4-color}/translations/en.json | 3 +-- {ps4-colors => ds4-color}/widget.luau | 6 +++--- 7 files changed, 15 insertions(+), 16 deletions(-) rename {ps4-colors => ds4-color}/README.md (90%) rename {ps4-colors => ds4-color}/panel.luau (97%) rename {ps4-colors => ds4-color}/plugin.toml (93%) rename {ps4-colors => ds4-color}/service.luau (99%) rename {ps4-colors => ds4-color}/thumbnail.webp (100%) rename {ps4-colors => ds4-color}/translations/en.json (75%) rename {ps4-colors => ds4-color}/widget.luau (66%) diff --git a/ps4-colors/README.md b/ds4-color/README.md similarity index 90% rename from ps4-colors/README.md rename to ds4-color/README.md index 3a076b4..8ff97bf 100644 --- a/ps4-colors/README.md +++ b/ds4-color/README.md @@ -1,4 +1,4 @@ -# PS4 Colors +# 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 @@ -13,7 +13,7 @@ library is required. The plugin writes the HID output report straight to | Field | Value | | --- | --- | -| ID | `Hy4ri/ps4-colors` | +| ID | `Hy4ri/ds4-color` | | Entries | Bar widget: `widget`; panel: `panel`; service: `service` | ## Requirements @@ -40,12 +40,12 @@ Re-login or run `udevadm trigger` for the change to take effect. ## Usage -**Left-click** the **PS4 Colors** bar button to instantly apply the last saved +**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: ```sh -noctalia msg panel-toggle Hy4ri/ps4-colors:panel +noctalia msg panel-toggle Hy4ri/ds4-color:panel ``` In the panel: pick a colour with the native picker, type a hex value @@ -63,8 +63,8 @@ colour survives restarts. ## IPC ```sh -noctalia msg plugin Hy4ri/ps4-colors:service all apply -noctalia msg plugin Hy4ri/ps4-colors:service all save +noctalia msg plugin Hy4ri/ds4-color:service all apply +noctalia msg plugin Hy4ri/ds4-color:service all save ``` ## Notes diff --git a/ps4-colors/panel.luau b/ds4-color/panel.luau similarity index 97% rename from ps4-colors/panel.luau rename to ds4-color/panel.luau index 8e5e1f1..48a2e99 100644 --- a/ps4-colors/panel.luau +++ b/ds4-color/panel.luau @@ -1,8 +1,8 @@ --!nonstrict --!nocheck --!nolint UnknownGlobal -local tr = noctalia.tr("ps4_colors") -local SERVICE = "Hy4ri/ps4-colors:service" +local tr = noctalia.tr("ds4_color") +local SERVICE = "Hy4ri/ds4-color:service" local function sendToService(event, payload) local cmd = "noctalia msg plugin " .. SERVICE .. " all " .. event diff --git a/ps4-colors/plugin.toml b/ds4-color/plugin.toml similarity index 93% rename from ps4-colors/plugin.toml rename to ds4-color/plugin.toml index df6e18b..54130f8 100644 --- a/ps4-colors/plugin.toml +++ b/ds4-color/plugin.toml @@ -1,5 +1,5 @@ -id = "Hy4ri/ps4-colors" -name = "PS4 Colors" +id = "Hy4ri/ds4-color" +name = "DS4 Color" version = "0.1.0" plugin_api = 3 author = "Hy4ri" diff --git a/ps4-colors/service.luau b/ds4-color/service.luau similarity index 99% rename from ps4-colors/service.luau rename to ds4-color/service.luau index 7f45b4e..3be85e1 100644 --- a/ps4-colors/service.luau +++ b/ds4-color/service.luau @@ -3,7 +3,7 @@ --!nolint UnknownGlobal -- Headless service: implements the DualShock 4 lightbar protocol in pure Lua. -- No external binary needed — writes the HID output report to /dev/hidrawN. -local tr = noctalia.tr("ps4_colors") +local tr = noctalia.tr("ds4_color") -- pluginDataDir() may return nil (state home unset), so resolve it lazily and -- guard every use. Computing it at module top-level and concatenating would diff --git a/ps4-colors/thumbnail.webp b/ds4-color/thumbnail.webp similarity index 100% rename from ps4-colors/thumbnail.webp rename to ds4-color/thumbnail.webp diff --git a/ps4-colors/translations/en.json b/ds4-color/translations/en.json similarity index 75% rename from ps4-colors/translations/en.json rename to ds4-color/translations/en.json index f2ca809..949591a 100644 --- a/ps4-colors/translations/en.json +++ b/ds4-color/translations/en.json @@ -1,5 +1,5 @@ { - "ps4_colors": "PS4 Colors", + "ds4_color": "DS4 Color", "presets": "Presets", "save": "Save", "apply": "Apply", @@ -8,7 +8,6 @@ "bad_color": "Invalid hex color.", "no_controller": "No DualShock 4 detected.", "write_error": "Failed to write to controller.", - "ps4_colors_missing": "ps4-colors is not installed. Build it from github.com/Hy4ri/ps4-colors.", "settings": { "glyph": { "label": "Bar Glyph", diff --git a/ps4-colors/widget.luau b/ds4-color/widget.luau similarity index 66% rename from ps4-colors/widget.luau rename to ds4-color/widget.luau index db6eaa4..bf0e4cc 100644 --- a/ps4-colors/widget.luau +++ b/ds4-color/widget.luau @@ -1,6 +1,6 @@ --!nonstrict local glyph = noctalia.getConfig("glyph") -local tr = noctalia.tr("ps4_colors") +local tr = noctalia.tr("ds4_color") local function render() barWidget.setGlyph(glyph) @@ -10,12 +10,12 @@ end -- Left click: apply the saved color directly (no panel needed). function onClick() local saved = noctalia.state.get("lastColor") or "990000" - noctalia.runAsync("noctalia msg plugin Hy4ri/ps4-colors:service all apply '" .. saved .. "'", function() end) + noctalia.runAsync("noctalia msg plugin Hy4ri/ds4-color:service all apply '" .. saved .. "'", function() end) end -- Right click: open the panel to pick / save a new color. function onRightClick() - noctalia.togglePanel("Hy4ri/ps4-colors:panel") + noctalia.togglePanel("Hy4ri/ds4-color:panel") end render() From 0554daeb82d4f563f55fe7a250232ff4e67dc7a9 Mon Sep 17 00:00:00 2001 From: M57 Date: Sat, 18 Jul 2026 23:59:27 +0300 Subject: [PATCH 13/19] 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. --- ds4-color/panel.luau | 4 ++-- ds4-color/plugin.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ds4-color/panel.luau b/ds4-color/panel.luau index 48a2e99..a18dac9 100644 --- a/ds4-color/panel.luau +++ b/ds4-color/panel.luau @@ -32,7 +32,7 @@ local function render() })) end - panel.render(ui.column({ gap = 12, padding = 16 }, { + panel.render(ui.column({ gap = 10, padding = 12 }, { ui.row({ align = "center", justify = "space_between" }, { ui.label({ text = tr, fontSize = 18, fontWeight = "bold", color = "primary", flexGrow = 1 }), ui.button({ glyph = "close", onClick = "onCloseClicked" }), @@ -41,7 +41,7 @@ local function render() -- Live swatch preview ui.box({ width = 200, - height = 96, + height = 72, fill = "#" .. color, radius = 12, border = "outline", diff --git a/ds4-color/plugin.toml b/ds4-color/plugin.toml index 54130f8..6f6cdf7 100644 --- a/ds4-color/plugin.toml +++ b/ds4-color/plugin.toml @@ -24,7 +24,7 @@ entry = "widget.luau" id = "panel" entry = "panel.luau" width = 420 -height = 520 +height = 400 placement = "floating" position = "center" From b26d32839562e0900153a085df06a8870741ac5c Mon Sep 17 00:00:00 2001 From: M57 Date: Sun, 19 Jul 2026 00:06:11 +0300 Subject: [PATCH 14/19] art: use panel screenshot as thumbnail (960x540 webp, crimson bg) --- ds4-color/thumbnail.webp | Bin 11156 -> 11284 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ds4-color/thumbnail.webp b/ds4-color/thumbnail.webp index 691b7da0f680f41c33065aa31dd63a2293d2097a..9c34c6f6b6f363eb3bb81495dbeb0c8207681377 100644 GIT binary patch literal 11284 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 From 033e8f5bd29ecf73c743317e6bcf8a15ff404f50 Mon Sep 17 00:00:00 2001 From: M57 Date: Sun, 19 Jul 2026 00:11:03 +0300 Subject: [PATCH 15/19] 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. --- ds4-color/README.md | 8 ++++---- ds4-color/panel.luau | 2 +- ds4-color/plugin.toml | 4 ++-- ds4-color/widget.luau | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ds4-color/README.md b/ds4-color/README.md index 8ff97bf..9c8bec2 100644 --- a/ds4-color/README.md +++ b/ds4-color/README.md @@ -13,7 +13,7 @@ library is required. The plugin writes the HID output report straight to | Field | Value | | --- | --- | -| ID | `Hy4ri/ds4-color` | +| ID | `hy4ri/ds4-color` | | Entries | Bar widget: `widget`; panel: `panel`; service: `service` | ## Requirements @@ -45,7 +45,7 @@ colour to every connected DualShock 4. **Right-click** the bar button (or run th command below) to open the panel: ```sh -noctalia msg panel-toggle Hy4ri/ds4-color:panel +noctalia msg panel-toggle hy4ri/ds4-color:panel ``` In the panel: pick a colour with the native picker, type a hex value @@ -63,8 +63,8 @@ colour survives restarts. ## IPC ```sh -noctalia msg plugin Hy4ri/ds4-color:service all apply -noctalia msg plugin Hy4ri/ds4-color:service all save +noctalia msg plugin hy4ri/ds4-color:service all apply +noctalia msg plugin hy4ri/ds4-color:service all save ``` ## Notes diff --git a/ds4-color/panel.luau b/ds4-color/panel.luau index a18dac9..bfdbf5b 100644 --- a/ds4-color/panel.luau +++ b/ds4-color/panel.luau @@ -2,7 +2,7 @@ --!nocheck --!nolint UnknownGlobal local tr = noctalia.tr("ds4_color") -local SERVICE = "Hy4ri/ds4-color:service" +local SERVICE = "hy4ri/ds4-color:service" local function sendToService(event, payload) local cmd = "noctalia msg plugin " .. SERVICE .. " all " .. event diff --git a/ds4-color/plugin.toml b/ds4-color/plugin.toml index 6f6cdf7..d8c1db1 100644 --- a/ds4-color/plugin.toml +++ b/ds4-color/plugin.toml @@ -1,8 +1,8 @@ -id = "Hy4ri/ds4-color" +id = "hy4ri/ds4-color" name = "DS4 Color" version = "0.1.0" plugin_api = 3 -author = "Hy4ri" +author = "hy4ri" license = "MIT" icon = "device-gamepad-2" description = "Set the DualShock 4 lightbar colour from a bar button and panel." diff --git a/ds4-color/widget.luau b/ds4-color/widget.luau index bf0e4cc..33a1f2d 100644 --- a/ds4-color/widget.luau +++ b/ds4-color/widget.luau @@ -10,12 +10,12 @@ end -- Left click: apply the saved color directly (no panel needed). function onClick() local saved = noctalia.state.get("lastColor") or "990000" - noctalia.runAsync("noctalia msg plugin Hy4ri/ds4-color:service all apply '" .. saved .. "'", function() end) + noctalia.runAsync("noctalia msg plugin hy4ri/ds4-color:service all apply '" .. saved .. "'", function() end) end -- Right click: open the panel to pick / save a new color. function onRightClick() - noctalia.togglePanel("Hy4ri/ds4-color:panel") + noctalia.togglePanel("hy4ri/ds4-color:panel") end render() From d16372e27b1b62b0223f45a174607dda9251202c Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Sun, 19 Jul 2026 00:20:51 +0300 Subject: [PATCH 16/19] fix: thumbnail --- ds4-color/thumbnail.webp | Bin 11284 -> 43682 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ds4-color/thumbnail.webp b/ds4-color/thumbnail.webp index 9c34c6f6b6f363eb3bb81495dbeb0c8207681377..7fc8b231e90cb2b005078c46d18420d2b0c9a3df 100644 GIT binary patch literal 43682 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

@2^m|3%<|_VwpQ zAhfe}CZr8nSk=@a3gsEC7tD*gHVBijwlD2ZFX74G)ZHD$^sW~r(GWD+DDRo+(8Rv@ zbzfc6dl#SCEysaRLHz_i=a1vAu0Y-wuZOgRgSbjF`;HFjlRwZ{RPtr(PEdu=Bek6D zDM1yjPs4bxUs~9zhI6A7b=Jxq_j+<$KAP@X1mlY6lZ#$rBfNT>#p-mcUGv3t2Ha=y zF_*_oNgDGLi=c3Wl;b~qu@r5-bP~AN8}4Fj&l}Jy?rL8&@;ntPMqT5j%+75txH-`f zHY(G$5DSHJ5IuqST-ja(1_zWUJl3rCJT|5yp;z@I%V2Y*Z`t7~gXcEih5Q>hhPyex zU25ralOQq^O>Xz1vN*@wthAAkG<$!KW#AMj@#kxLR-D++HPB};K}M6{V1MHY_h8`6C| zj`WTMYjSOXUOto3Q3)q{!yhWvDU?C8*k0}m>r;^+3{EL`Niq1>(w+(pMR6jO808CX zCvJ+TL-!cd(fCsA)PE4uHNx-t+VBRRWA^ac8DTqkbD#e_Q7*G zfs`rAI`IO{ha*D$Pa|b%aY&ARRNA5SCZ>g9a)cOUQbD!y-X&Fu=j9Y+{0dFyARVT7 zDO5Z?lkA*+*S^pm7~WFYeRt9uKlb$LENtF>0o^)M4)(JejMBU*$Swn=$%nzjNbL#( zlcy(vVbN+YN-Ir_yGf+IM?U;g!dP?M(NDL&Sy^{_0!u!5g&;#FOz~mHkfc>K_W4-m zkxKdCkAF74TERNoJAxDePf#ISB>tDxvXIYik0d*P@&^+F`2TKnHaX5xaWMak9qYo6 z{(d-_&x>qa%A)LJnQS+}qa?PGB^)Kqhz^T8b15+J^JASL-#G)75M86#*ypU>T+k`- zQJOeYq&)JU{$}a~WyCouVPDc9Arzg&PyOOir$K5eYGUWv(Y{a#)Qw56 zRMz@f5@5X)8wQZmz6b5+g(^9SLK*8afyb~S(qA&{;}+2wO`ZO{UTU}jrMttW+#~2~ zGA{RF9hh_7bk&fAB><^39c(;EJ>%s+IZl@C5S(?VLkf`KHo6vl`<^NzI)GgKX}Or+ ziUR@<2ZD^AGL06O5_F!wb)JS4n&Fzo8qv{x|Pfthu;m*C>Bd)^G;rB_Q(`?$eyPt2bVb{X8N({c|O)}##TkdlhDvBTuoAM3BLE0SZ zF!`SBD%t5xhDDhitnLLgr48NfbA=)t3xPM-9hhz647yAR_#;)~;~GgY>B8b>6n2ck zOjhZ{4M({7O-8tO|3H7NNb!wIc|qs$H2w31C~iVkPQ2P8iE`p^XQ-o+M|VSS!iGj5 zvvoVf_qyK-?D$BFz2`^jt-cORb*nSvutiV*-0u_ZeNxJGo=OmNOOP$ECS*AE&G|zu z7h7>I@dsW?qLQgaNgS$XbIvKr6^BB@7{((qN_Ai<`2C0`^0;8GN_=s``=$80H8(dw za0BVkB<(XHusMxYaifN~E|xh$?(Wrn zX<>8t4cT9NDFo69qY^|MTZII9pbv0TA0%yq2tt4Gu2pP_nM)mmfXJ6IXrwUx#ilup z2u3GlxJLcp^)1_t)Q&W7R=WBaV?p^$S_Z9C!;-^01y0Ekpg~0-}Xh$ zOE*g^UEyhPtYGD(Z~h3gnf73UO_-2cSpj0PV%t{%<0gXt%2edHqJdMIv>(iNVuL2j zMz8y#kUa1oGBC;#r4@2?G<^2_&*C}@tet7rybwvKg{ChLvpYIOZ1`%;()>E}T{?G7{X>?w;wZXNOE!*nKS+{L5-x=b9L14b479IJHjcuDqb^NiOOjZtV#>BX;{pO=4A3I zUZ`ND3!+^lVWqDV)gPVxt-(S}i-f}Pc*~5)DW^9~v}fa*NDcIr z8r?;3jWhP@%iD&psJ|nxZN$G6IT)$KVAgRNa=ap@qR2SvSz2D_W9=%*ZV}N!wv%r$ zOVU#5V{0ebc34o=CH8jLube%YDKk)tVcMTGt3;_U znW|#&)5W?Q?oIQzpr-;;1DrZ{<)q-?8WgSE->5Het3z_L)0MkR9m$_3VbmQuP^xc+ z<+bOj%G?6`@ofP_ud8i?XwB&6Z&R3qucabhRDwP14G`We6Uhy^0%MpL7=i7vcPO0| zj($h%`@1HVAiqUL|NT+>zl2IKW4<>u>rSk`5YiV#2`_WlVQWi~q`t(zgjeWzbW*qS zfptc5vxxHmAG-Hq8<$<&?GiHtt4Aa*W3B5Uu}&9-^4PvW+Fr3@U`6FLMQJBBveC(qyte$K_7Y(f(@liRwZGGBjvgSO zjztEV8h^oZ-6y(;zn*V&#}=u_0|G>k;YT~;Tr?rp?fU*yC{AR`L-fvDV5F*4ZAD)! z+%<{ucZq&xLem7tZ^U;N^QsVUGme14KPvAsL6xmv$L22b|JyqN${acAws{jsfSR&$vZmxRT7T2J$!;-p>9^zF^~Jug2?X_^GEtUs?`SA)p;bXpT^%iine2 z507`ND?&xbXSrL`+f~ibeaW|q#lH?Nn?R~yCD4r_Jv(&1hrFW(ZWA}@8QSyz+H~%A zJ%3=4G^v3(rhp2T@P$7{y(3#<_ zy4g(6qJS9wfv1F$3;FRdPXM!kc@JI)^z{O2G?ianna2S%cZxBpZrZX-JGa_^yH@vZD2UXf6Kx zkyvt1npGYE#6q8oR^mRlubxfsH2$%KrJ}QDS%=aK3fEH2>HUhJcOtu~eOLMBaZdG@ zSRt<8^gx#rzC;|$E6%&H`4#=E6T4CHsdRK3y7*(V*GZTNpIY5!KEThhbM%H|2@f|a zRi7RP;cyRYu3fo(lynfWP(WdUTFM6=4=4iaUr{RM=oW*U`=*vFomH|+fPi}&t{CPW z?A6M%bKrf7hSnTTm+w4MzDLf}dLO|K^2{jd#UAZRtw7Ap4GNQE7;DaK+OrECJw2Zn z01PE;RRoNgsz2`hpq%TDh;mlYYF+%m=u5v`;eu2XM#>*oaK`NeqM;J0imF`iQh|KcUijM07UlCzmnHV=VcB(8_B(B>JF4qDLQ z5=1N87$qhV&i7KTuY9g(LALZ2l0!@b$`IK~esoX}e>5|Bx;6Ln49^2egx^p1>*wpy z{klLOr!en8meBUXVp2=r{Ft;KUWxY)hwPwj{HS6^zQe0)&L{VYAo9qO-f)AMO2>6O zxIX=|B4#JXppklXt$-tYE)tw|HVeF;gRTB_m2E2@O74dzcP4PXt5_5FKI!joRg3o% z3`}XE+2wvVu3ay+STf9FbRR2Bv5$9g3U5|=LuLu0Dq3iV4Xx(otJzpMJT(M~yDXt6 z7iq2%_|P%W4TBd7@OA;9xEQZTzg&C<>)MMx$_hL|SC9OV2VC{%=MY{F(5Y7Yuvi_r zAi)&IL3-U)U;?~qIZw%^6dYHNKZ|_rh!&eI6byR>w$?9kon5`bh<*y-ciZTs`^t++c0y{?|zH&hC%l>qr^0`YW! zx#3HbPP(ZVF==+};pDcpK||%M>(mO!hqvW)qVvlKqwU9?FPfLOxRKIr@=xOR z2@G08wLa>K{2YZWI=u*y+&)3UijRf^mUBqWFcKa$y9JEzQs$T}X500{Ne{PlS|1nd zG&dTYzpq?e#4b8YT;-;$!oY;f`ws05`?J>F8_c>Y7a0F$I57I0^u-AO`H{b`J1F>p z6XBziWB7s6_$GR=Z(7r1@(qjth7ok{YCZpnHCEeD3BysxQq{CNRZ2 z_mrfTXa62EH{s7dB!gwa02s7Qn~ z^;1$aV{|*+qCDqUr?4cKdmMP_vxRu<5Vo6mUuptWb)kxsLKNk$Cy#kVV4&QQ?Tq)# zF9gq2Ag%S&WW%Bf20(%RT7;QP_cM}%G(!rr1!_^=0VQ1aLGarwae8 z35Fpv1_=^6pa_&)Y0%mQX87@pO@lJv%c)2H^v06uJuKL|5V)HfsPfGtLhXrh z_>7Xf8cGR;*jg2|I{vLG08!~gcmav~aFAJZbdZ}G9HJK|6lDhP6RJxM(Thy}UL{&8 zrN-KH9VM9K1wPT< z*X`3L2V~+1jfl2=M%&kF;#VAZmu4#)^95`4S6%_@!2oOh8m%^n%sp%irJQOJj2TRZ zd^_Je_>EG~dA7WDBL7MNT}pIp6|P9EMuIz^5F~bu5Ji@kMYHKORnDwxFVZhr`9$0q zNn;fIFQu32GtU~vIr!T-LoNN;Sww~;(;>5)eah9!25l9OrAuPnrUCn@43ABOka_z9 zYg)=Wc_}-X^)EF&X3jVvc-9WT0!8+(qDfbZ(oqKdxpv*2Ter^lCm*lRNPcR7L1dVg z#DjSFu*u^MKkj6+s+1>$YI^MO`Ry`tggR@SbW`mPCQq-uJ8HODTA38gE}R^B}^} z8QYe4o4(;Yywy>|KBU?d+xmxPo%L76gEq2IKv_l;7}4m zOx}nWfw%Ao+B|7%g-m&92#SP5)mOsow9o2MyzV{1{dGP}2eujB*^`M}lW(i*_ku#iAWTU*Nhm^l=EL0?4;fT}<$vB$5NlKf zCo|h+mY6rGyxra*kv-Cu9!Qa|L(@M9KnH?|ibAO`MY~mLNxQ(oxN!YN(&UnLe-6CD zyZ06zo~9HgFkNTS+(HaH+xq%y{hvl|!v79{heE%z8S{}RWkv3eY;S)Mbp51WBx@vA=9l+7>-n#r5boMnAwBpp8OObao_c(Gw4*8b$);t6X4w9H7 z{AjL>>Gct9KK$xx`Y8yLMo^FoGM5_^{0rlqc+0N_OBKeII!)MS2j=(n8RAn>Q!7#skv-q}xRg;7d?uNY+0quV5rM6?07T)=6NXoOlEK3L};N zwd`6ax2ZZGEfQjtR;Vj{Z=P(iJ|Ys}f^hKTM&TP&$3W&e)_uo$4enIiqX-Mvj(NkhWcNolFZi{6D80WlnHiO>tGY$I&EHn}&+n!4MT4doL zLDsNeq7GidSp^%WQW-TP)lO2vZ;GGecxNL{^bMju!M?p&1k{$0D& zB0qPEiGCTh%pO&Kn?x`xK(pf>CLotl+Vhzc$`=L4wVPhB7kk>=gMOn4grg7jU?9c3 zWLN~0W>R!by8+bdz-b8hTOj5Xa`ad%gpexB6XQff>N3zx=G@z}CDMR5;CJ`^P zuh{576U=CQhId*FHj&J&5|QQBWVGWQqY=v9Pn)vCb3B*$SBi^Ngv&zG4U4vBNUqfP zSU9heYG3gS&$9#wEA!od0R_n-c=vlf1i?6DGAX)=4$%faGVlqmyL3^dk$9>JD_@K= z{0hH9UvVZu4sKgM<;AOAqKwNFTl8eOG?R6De$8b z9!m`td>>zDVS}HQ@W;sWaC|%NkjOVxczEO>I(TQ?6-|thmuUjypS5CpYyB9#IOTIH zCNval(@F;MwJ5-k+k0u`A80T)wp_GVnUEr4U{Vq3VjDe?RNjOqUwtoqD4um6Z-#xp zqg%1Be;T`LH1D7zmKk)t&34BM9>_&9jt3mIaVgPMb_PmlNh7>2c(6PM^OxiL=XS>F zT^cf%(hbOcEYhzm5V=w6IZaq_m6*Z1hr%UWgtC6#Ex+w3v#?2k$dgR5g`!G8R-K^z z9y^c3dowuKh~}GQ2@Ee?$+9D$YO0EjfnfdcvLKnz>P@VCe7w1L;un#;Lf%-TN{fxq z1H!I!J?Tw!4r5u<%180x(@Hn3Fka-a9gtL(%-g~Igc!;=o&>iMOs!y;iseclzTfJs zA5EL=Dabw#C05wG3P5_Y`-WX-+2DMp=NGE8cA_>%XO^Dhf}qq|?9Y1pQA)}vD?UkP zC#99_aq*~Aw$6E|BhkHJzvd!k^%i;>kjU09T2&P8{LX?)RDBwjWMmD}rqG2LYomKDuj zCjVOB`)i5Fu*2pQOz1b3#$S^CzK+hP8boWg7l8%RX%2^vZ{?E~lcZnc2s@gjGL8?t zEQ5AL0l$LJp)!ca#}TX|aQUkzcy!r)%5Zh9@?6%; z#i)cEpt8z`aUiolY;pCZ~w(mig{zP^rf$)t_ay$(cXkaK$kE_jBn?YNg``HSti& zt!OMwQ~zxtxq84nANxB)T_Jvp!ue{3D9<_;2im?ELaHRwXiYp!7q~v}ICXqK zYK1k+m)<1nv{d!KEkDxTvkq-V48Tub=;cpzC@&HUC@GN3CMr?6gl&o**Ex|*h$C7@ zC7m4;bR3%ERBWG%OqFBFaKLe&od#pI*&xrg`5u{HN#DkHvGw$*_I0?cHe&VfwVxMY zxOje@W^gUmem8PL^{zSgBbdXyovt`j16fiJ*(lmYfGF89b`gtDhwe*P(5d^N5}@ zBbDKKMPSC=-lPRLb!Q!9;#$6P{SsYy37#v%dNuZ@{iyv5(ElwGNRHiq&R|s^lU?Gi z7+3-Ycf)#^nI#z#QriTj3G1Las*!VgM=i|ZmhuOvKH*23XaB&8tXy_c&JGQ#%r_=z zgI!cE;-h??x9B?7gFcJZvSSmVO2Pjt%`iPFBoA>(bMYD_zhsdYWoWmgN0Ix+aOzdM zoKhG3Z4WmhnNBz|N&fJZmWIPpM-5}<2Y$@Qe>MZKBgc_B0SYPuF{h&}II8<^=-7_g zufensm40clb>W@skQJ_%rf^_-&SU?fM^yu*C1HZSGlD?ww%Q0ETXB^`qmO<0_} ztl&)?(|lYd@6>UM7!!WACyJ=7ra7zin9?>Km$gdU^G z!AqvJy+sZUO%gGJlRWXX<)*Z;t!DrSK zDTC~%kD?7G=p~5P#BqJ){|^9QK%c)_gk_xBV>a}$5wzyZYOn8iyNyV3okd}KE^vMf zdbP~h4viU*Er#$NxWeaDJ*SA_6^Q%EP_CXTiI0Mql1A_H3%({Pj|}vXtI(*M`)dx2 zOj5E`dfp=CDL@Qo6yKehI|bHtqDl25TvopXil0WzQ^NO9GrpHYP;^gtyJ!|Kjjqjz7L{q#U zLn|mu{&`vif0=hrC{A8(C8~PkK+MEDEgiJMC@le*uM;;xeETC9_ zaMu&r!vAMvxi;$}V0|lyju8o`SdyO)6>eLI&Oi^ud@E$-IBnLHF}kT;I%vu7Z}qpCU;)BlVkNQgh>qk>qO z5Jl|aPHJ!36%!DNY`AMZN9UDrk z3Bkv&AuB6)3W#wcc>`K9uQHmP-aL1S0{7rYS^3v?-Ff&S$L@{RJn@zy$Ydi-00000 zkYCgC69RFE3|L12f_n6qkYs-j#E1tA20uUwKmY&$KSk`HJ~@{ZTT6vJ^a@v(bdPiU z#Hw%FRAUoSv?6#V8u;i@|GU?sGA6at`e6Rn2I&18m>?Q~ZRpkbg%Go$9li|65l24Z zjeG?+%qs-@PR_WD028#EI2~k2KYW((NTA*fg2wUlhmNwRm#+l6$=WU%)ujNg2ph5= zv|wSspnvNI_NbMcd_4b-`i)e@)$P;XfSK`Pzm~oUh7m2MgAd-m%|D0{DlZ|2mzv;u zV5{9)!QJ33skZ|#!5NhT)Pq@XvWQ1MTfqMlsKN7#R(HR-s2|M5u*H0^{^!xIK5jo) z27e~P2gFsg>bZG~!l>wBUw1(R`#YY(cXo%SEhpk44@$6x&F3P?AIU{i*z@bDQAJrYlq5^s1_sdEIc&$3o?M>7{t=C|0KX6d2V0+`p}d(pAXCtmCy~6(q7`(W=bjb0LOOym9u=xnyZ+!b zM)+fl*x-&1rZ0`>v>@^{ z$iY^~jP+2t6+53yoe|_>QOMqJl>-7me5CMX$)KId>Qr zp3EEg%8U!Q`)j7CE9hE}(Du43|Ib=S=&QZ!V?@k&P{f}G?r*W%9xJCLBpu4)z zZDt2L_l2r8S{b+XWq>n-|2!1re$=;1cF#;_>@&Br1%=IA?EP}=*&^&B5{~0RDJ;Vy zZlbri(3|djN@Kh6ch(1g^|0xkk_W^Y-bWaG@CAh=$shM=s{oG(+IshZPWT2UUoJQV zu#3#t8KFLK_iC)Mbh7W}^l#Df#OVBz*X@*KAdBi%?E>ia)I z8rnnTh2j~ne>&Sft%8Gek)YLOA*I2!AS)+qfP;z7ydO#v zSX9a{+)U}!WKSD0GC^;{7swe@f@6u-{MbPK1Eze_{Qm@L@{3r?T-a*Yv}abWU2FjH zlUWkmKm5o=h4(tB`r8j9Px67Mf!O}l)1PB*SxCUsAT1Zt|F!_t;uSLEpVW(7@+7Jv z1zW_9hGrH^HPiba=O#k1lTB~M9KmYpC;$Ke2+FyWl9 zVx-56;>!ZGU&rKxSez*B57Mf zbiOMg6|j=8k2Hy}__QZnYasK{ozPSt#u(O?^7CO$;*wHe%f8M40ouY=c8OVK7 zQPltog%vscMb%#gdMNlFYF3et+~K6klTm@MM7(DqQh(wHvL@tK^t87@+ejB#&pC)rii3(;0JUbW}d8_qR0Ovdbz60Bc z1(`4lSm<+E^v*a_EaL0_T1tB;`;1;3IQb8x?LoN=jzAbueuJsk@i)Rcw=p-+@!e%i zyYFv4x)JEq0&b3ldWU9h=352(Yg_5d8WH<5{b$j}wKXFAZvf5ejrQK%w3_V#B4r+c70h+2L}iwYv9c zi+yTd#kBL(0|B9&CCu7#eRi>XDn#($oTpJ>jXae)K{!Hh(oqU z%nyNB6Cxvb2hk2eyGnTm$94zBLi(2^KiA-w9dv+54eSc?tecT_|Fq7B7Lzu@mmSX4 z{--E6yL~6^%|qV7$b#+EdS{6{zi$3mjY!#FiIfu4BQb85TiT-}f3ib_uy^^^LN(&Q z6F$`p164n)7$I@7n|3xEq%W+MDxpR(=@Q$Pq2Z`hbzRVUFh$pmwZ5SR!^0_Xx0R`__Jl5y88%z`{14eE&Os<3s^V}i?rT z#2Dy&6<@iv4UWbXbPxbw38+@mnqonyd%b-;(xqn+xT9$x^r-HF`0FG3N0U>x)*!w^ zVo`(XT6raT5!F1udje8XvZ>DKY@)On^KM>gpl(P|6g@xGR;qtXV)yE1Fh^Wt*lUe` z$-5q+FhWuogmx_5;?QuziiKHm z_!ZS;P3lXkaVA`q-e8xBs?_co6HDB{s19I&*_1j>GzrXuyn!3_2=5(XufoWM@3IX3 zf<&Q}9qYZXu`8C|bmz~_9_i(V9+ykkXknhY12Gg#^UY$J=~hw8056SBSfh#?78BJ~ z2T?11Xob#}I=w-uu($5!_tW!I=R)!O%mTTjivQ{|DPGHdr*S7~Z6!6S*=SBwP;!`` zq8iohwU=e?5GoLE@WArzEMGtQdu5#w}RhftfApFP8g>a^Bb9pNP&!J+eM)Lq1IF@XNL#FPSiQk z^8Z}irGaKbM+h z&|C%37WXKx%Wt6F^pT%^jr>Crfkn_u-EcV*;t6^ZDy)HR0SzAd$?t4w&?ZH)PFhjL1a!`F2^;EhyBr^oP9i)2Ur{XYeXn@n2LfR# zAe{S?L5Z-!(hX>oOt$oGwt$bA8+oalfN2E!Zy6(fk5*Af`VJ> zvnXI{-$dh32_d!kBCv6pnDTPaO5raU72psk{s}f+YjCm|?%V(NG@wM&@a-_LDI7Nu z7h7s{>~JV6d{SS1ygFY^0(3o*1Xx=u{7+fEU%qWSkDiMr2_WHA3ufsOt%afE9gTt! zex!IAo`p%JO-)Vaf`-*I-gt7IC2AL92~61vYOd^oY9_1 zT)1U){~R03MO{?PQsyTjyaLc_FsjH)ng8Sf5VCTIAE%Go%b{G=g2?G#fv+9O*9y6$ zFxA{f8`g}Muq^AhBgITxV&dEuOfIAuL={9QnOVW;eJn&>p*qkLj@sp51%yn8fY{Y` zt~~gNis%nxTsm}BC{1ORP-MD}$3VbT3rup}7buWLJOR)y71o~~xbkck;g-6jQ~M0z|44MUltY7y zX}zWhg-E~kciz_80n>RH{zWb1(M|>-v=DrD0wWqUD$b%{h@3Oz6{ACIVXDwK67x=P z(Oc|bl8!hiHRC#>qt+!WHL%KnQi}@r>ma^6f@jDuV3PgbX?r*-w$Sec0gpgD+s`HI4 z!QGhlWw{icZkq`}l53F8H?kJ{n3hsm-o zQ1WmPL%IhIl}!j3aJN)we&I`5HLGj*~C|EjqvZvtx=RgY||f zoT#~znuE#(Ycqe?-?trFM;%Utg(@IzNH1)o^zV@5g-}2KJU(*Xn^%sBs4!0;&qrUN zos(|k+tfunNiJNb)P1aoUQA~N+v}UAh;cMZAH*ZPOxLbK6@^`rM%F`gLH|8vQ9gfm7#E81Y6-ZAkD)_jg|2em1 zNo)EQjwSdwTh|j&d|N4GeYO}g%N3UJH8%1al~<64 zv1+1z?K;a!R(AYVJv2+v_fYJdnb`c14U%;gDy*g!#1?G-QXB3Fb^(6L>w{~dE=8V)4Tow>>?f;U}-K;fX2b0hZI zONdv2uElG(m6|8&#=+s`_Di~0AGQE?_AR-6ajRsW9tC|aSh1IpbS|{~F;|3R9b-m#BMA9rbi=B;i*(BRg zESX#0J$4eqIYjsZiMD(QAzK_P(%R6q0S~;;_{lMs|2DAYO$ksPuvT@P| zCptt6&ZIVR(p`KArD^tF9_|WMtPbgD(-4#RRB_0dng*Q^4O7=YtfIY!g^fKqZ9wOp z$!LD4Bsu><&^P(v@Yv$I_8YO>BT$!SH}o`DH2(72hB_nxDLwI}`FaG#Ie!woivu3T zM^k5tx%pWRQYVbZ5%yNZuzxQ0qks5TO8@Ra(|elp7sb_keRTr(E$~9PpwpbABDdQ))irrN{b7^x>R*E}Q9Wk*ccc#DtH$rj877 z$(o!nO5J6ZuTwez8giUC=CdjLiJT_pH0B_@!+6;2oY!nK>vq>kRt7YUNTo7!!~xUs z!qk1wKL+Sn2T1(M==~}R24ZPfMTiAomMlz;ABL6DddD1FXl2(vuOgEtes#IwyFlJX zKyeW?y5Wu7BA2EQJ`y@CG>U0;`yXPgNn47Lu(eXUE~+iMj2NvIyI%_X^)yBi;~3?( z2_Q0R8ZA_+=l7X@=YYj~FEfM!myfZ0PqYA_#)m%oOuqv9%je85CCWUpA%}K@O9oC$ z`0gym)K&+bW~i5(3>+fTx6fZksidbbX6{|gjT&z{+|rlH^fni~+x%ZMw=1DOvM7F# zTwOCcHg30J6QAI@73Gq%$Zi!O$HN9TT>8f2{x8H%5t(?>#_0bkPYp<r7+V^cvB zE?;-qsO|O8JlsS+U~NfOFGTY+ zORwW#3;z2v#*n-?WK{E>wIlK-5wj{Cj_twmV4S&^JM{@UNkH#X>g%)e-=k2_~ zW#2;UzFDGaxCzRrrl>#>;hbyb0F(pW{&8jU>cY*#vNY^t2n!bij5{q;QwI;e9AI>} zlQnUQg9&NVHG^h(akcsJgb&R#Nh+ra;}#=iC*#$)MCr@PPae*jD53YSMYljF`sIZL z1?P}D%7zr>uTr>Lvct9Ir;M2WeB3#bt*A9~Ys`skI;(h<{LGtzCG2u~>N zCD=4gV>m0DzSTmVMmqC5pCT}}(ZiU+G(BFagL5toaUJO~PGTAFc6HB9??cA@Xto0_ zSdS>D+B@7R%>n|*m`VbiOsS)tKt%?q;v>jbL9kLCVMgw(Z4|T3SdK6;VbU7ImFT}F z-a%dU4z)S@)OO?1M^t*Z`E$C7_zM+|&Ufx6G(@1^)G9cbU9s?8_rh-KUeSB-aO*VOkT^^FCqWQ@Qvyd>DcT6JIx|T0=nuON*E12uJXgd08A+Gh~)f>}l*V;d8$+(`Q&NdA)jtvWV2<^V=SfGSt@_3zz2 zIk4EJUL$YRw$n1!X^2hwu^i*ZNVUmn3=#i=Sl0p?imKM8lhlIO$k5A!-#n}ngT?g! zY(k9%H)(bVZ#G|d5@!|@vt=}Li;J3k4WgdbG)@soQ$BldL4%*x7ZBaJ^k5bTC81hHzKb7Mv}Vpy zTSlD)!=gki1_Vz5eUN)L_xM;OLH@0YKg2;{PruwIb6TCbL7lnomOHW&*_U(fYqQwP z_y3K=wlyQsZrLxUrN|8WvL$CfI>NQh8E||XJr2ox?{dbIdW-E{Z&5J|9 z1#rsbge0yrOo3H!q=+xm5Qfb2%6bJ8!xptI+sfFw4W3UFw8o1vDndmkY|5*pkaZz| zfLo;<9CV{W4ae%@N+nKa(!qv3_XK3JV#D(N*4=4wMeUy^-a+aS&JRbJbo zbZ7^9BjXb=AXh1v@FAQ5x9rB3=xfF>n`+eCf=8g|iY8sGcUkG-eF+E4u&MLi8TFD4 zp*i~#Q6V&Lq@QCE45mi zql40sIRe_lA|MPrKs&&EXTARM@$7K1uXJHU(jv?zURxTS`qN-r+sxB+$}FiCaMH}u z*7tXJU@&yX&We*d9!Cs_3c=Aijc3RHzrjAHe*Ywp4r1`k_?zPMSK!%-z-jh6W`%Z|I!T+!>5odxc~qGI(Rw`@Pw{X%&IF}L`~1kR zMwIRD+GIFeAEP^~{6}i@d=F8QcXAPA3Zw2yi+~~e-bd-?U=h6}QIb;fl1_=~9C7-@ z@YxNu<1BRK#Lz1B3MM)y)`c(v%U4X6HZB}A%ctU&fD`>e754XP*; z9C&uUlWq7gml7~2v_TZ_e=2<2!t#kRLnq#~pnKW2ZSWglZVjW%lv5TT+_?uu%l$CQ zwwuKFwHCL`!8JRy9~nYfPWxfYsLgWVg!_ILyACXMacw;;emG4-z5r4qkXKvR4WvEP zR~g5h2VRO}y)nTN;4^0@UwO7~R z_m0fjt<0pZN<5j?)QBjY>%4}&8}fFT!C6u?c7M5ZSmH?##d%L*I?=$w1417I+|Ds7 zd2{4F9ua8+&oQ_oH?e~CDO7YsX6?8Sm<(smR1%mT!6(8PQBdE9da3+KzRIc@nu&e* z5Z!yxcooGYI-B^;C}*q97`aoH?*_X$Sc1BqjJnX)fm^&n`PBD%+ru^ld!+6-S0CO` zHB{F;0e}Di00000CftEZj|Ln1V7O~EBXbTElmK1KNQQm;RuEP{LemZ~%yV}92X8~I z?f((ylhe39j0R9S!&j&>b0epF5sM_vu(`2p0k7gL^mD9{rP~p{2XZ#FiZQnJk0PeB z78ziD#ntKTD}P)?(IzBNwrP-X670$ZP3eClx6rs=E;+U<(`?Vtg|+VKn<^HFW4La! z*y%;w3Gz7%XHX@jOUjx#{Uaq27)L5Hof`uhyY5eA7$4l`drPw$c>I!R>m2!aQ_y+K z0H45T0&CyR7qc!o^!#6Qp=i|Abq_OETtjsn-(1fSooLbC1R~hYk&P+;pkt4_H^{_E z;xPst2&3jYcP*%yN%Ry|Aw+x7WztOut4IAADeaKL-J6j@tYvf3ID6-njw;Y`)|1UsQYLm9xU z^YCBPrG-_8aMB*TXLmtD)8VM}ZRpxy=b?{Utkl)WV4wA607yxK9LE8N`ZePQcg`-9 zdUOQco1gP94k!_I9`_|BQ4#yZp?`{Oc1IhPf$woiN=cx3^Bx0meC~x%9#Owe#`h9M zsa*Si#1N~He~3&0(PM}VsB&_7wWRmkHMYvmXzU|*=y&egt$Jt0)bDROSg(gb_kM=4 zn&yTh&JDsWt=mx~&j!@CKus^K~p(fNB-o9^Sl%!#RQ>c6+8`)RR zaM7qIs_}#2sE58le~}vr$JRjoQ)})6BB`4g%e5?Opre^cGh4@#ti zk5CV16vcd@K67#2id0&7Y?unAtBD}`jS@+eN(!Df@})1r``@96E?4QE!P`^i&sjY? zzkXS&Y3bqaR?3%BHm#~^p&wR(0W0Z^4~7ikfaB$oaTXvo|8mRC#NYpopT-q1>HthQ z%vsCe=nt-EniIdO0Ny&#GeEf&&hH9{J1p~QPl=YG+Sbs^{~^hg+-Xn!0`aho!~^3S z99j0#vA2TK;?KIzh7dn}b05c_H;~!_7AA=yz6&3P`zh)J%s-q(E%>J>z`@^U=gYdl z$e{RV4^HxwnJzy*qwammmO+V*ZUZtxRh{5Xx0I>1vC(h%4b*7;xT}OPchNB{6xI+>RZhQUTEG>(8pe<>UaIF#Gh=^kT76U(x zW5ua=F*9SEib@a^;4(p7H+@4|tz_OFvYH(JG8Rh26(sobx8UmmAFs#dFze@uE?at^}KgWPNXwQ5VTpTTwsPC=mX zo&Z)tW?q>ZQd>y)kI%*7=-3W8zDLt=0AJLA%A;A#9yTB_Doy`aL~v-SFztVP$;Rv= zFO?VR?m4U%~|)Q@eP zPj=eHuMxEeRuTb48sxtB)gxb@`Y&W@Uhwu{M^8=Uk6;RK&s40MZGr5W=}^C$aCU3N zBwt*k$TZd|EaNc?<0fX_jszni;_*j^LC{fjV|pP~LzM|{lu3;a?lqEM^U#-i+kqM4 z{Omn7qf8FUkpg-FQn|WS``>%`O;Z!h@|!Mi*h7X%-*V zEftx|7H!aVE8x@DIBrW|kqm-h6y4S&f2uZpG0|j#BZ_>_uAfv4cpC>NlR4vYR<22?z-&m(wO5d0tYN%|)D#5^t(9E4XIF;36^4x9@w(#)y1#RvWW9 z;pbIWDFFj`q-(2Wks%@__z^%)K7{1p#o+8mGhj35pJhFgS|hQ% z?U zHyGcIT6Dc73k-%H9^1kJaXL_QXT%AWFv(!6!6U2Oo8uJC5`_(?N@;y^3eJPbby(|R zGNY&uE2T1Y!-<>5IUj0P@()Ba#QI^k>|R}_Om$$8okebfr6a~aD^Ld@m8wtn@b&dT zD6o~O6ZJuo-)jA4QPhx#3hN{KkWFe-jLq7t?sOf-Z2HAV@!UOofGDOBRJfE?{20Wq zh=lU#6;W0;yktCKCy6zVxB@)R&_h|G_Gh0?=WwRH5X03U-+$U0@ZJ8imdfcb(RW6n z>MGQM&_`Xwr`#Br!=~G7xXxbeCUn`f>TG6<*I1)RD$g|-U@VC}$nF4mSEzfbbW1HA z{~yw*;`&WGRqC*T_CnRT;}qky_WYv!__7E=en+kD`>eH*{Go#GB`_saN?hMmxH2BF zHnK$*#BnEe5-|J;`l2zqsAFh(K<2m z8b*y|pX>{Ay9Ohn^WOKGCuA_9* zkf^8?DdiUYKa?`+2JLtr{iQFb&OLoPbGs%bo+DU!amHGi$R* zRV(j!hZpld1+07WDBS+1ICuibNq0K;ZDhE>hEQCBLA7fIrk-+sUG5pD!0kp7BBfvv zmR03@M{V|3*xP^fr#AbQq#UXo4KkHfFL?v8~RSpE#t_eesRjdl_*CTsRmt0 zHqMy;9-wyNPO&lnI_0(JP`ObSdf&Q;woxxS&-e&%Ak#8&nSvZsV)wJqJY8dFdq?a` zfm}+rIQ^Qzo}Y}+H=0t0ebndL0i~v+T7UuZTIK0#=FA6ziP5hU|1((jj5(D(NO`@k z3cgr-SlGQTE(;p&YQyR%Ld?NEM?iEwP05G8&Egyr!zy-$x-G&-l^t>e>VO5SInyO! zmrPL}h@-goa%_o&z^S%`=w%Dt7t54Yw`MV_138Lcb-Ng8Vh|Fcso;J&`r+=T5A95w zii?E#3cZ%=h)58v{8*)KInvsVD)}FKi0KxXz>FfPb7Ci+o)(5$+9G{q^-gu`wGdrJ+o3|14Uks52QnBQeHxL3Y zwUN3pYnv|pEAIh9n`(7jUJH0Bnv?C&P6Q@stn>}9dkIxsY-BFB{pve ztw|f88@P;WL7mg9{o62zs|5UM^UPR zX-W$EUg?ud^#O7PM>K-&#Oi25CO9T{f4y4E)H6_U5@brEm>0q%L!YHcwK{;4WnK~2 zb0-(5qF++1)_9geqK9M=*KoC21xXQD!u{(E6E(Pa;LNK2qx%W9kW85SB>=ZFRm!WAFCqTcMw_*1uzkW zFNOiD{bxcnp+GQQS-qgJ5i{S}&cPmSmgFHK>emK7amsw zjYq)d&jIfu{;qfY%`!=jU3$}KhPoFtY5N2{;gMr0BTb-*yhf9QeB6H6{C(D3V>DFO z-c%?CjWnhYZnR-0FAbZF(w-)p-SHo|5^1Fo(Ph z>-n=WyySE8{736U5ICCeVs|`%!M@0?;$b)Ep`XaGKs>_wD974fIn2=&v%vZppC;VT zF&J28_Oq(+r*U!WSeHL--D_6Sq~9<>if8_Ramn*qA?s)Iu`ZvDUjZxGF7i%~h$+g57ow6i8QUuRXB&_7y$U0i=&`a5Ok1i41nWNPSavI8 zLP<%E{QwkSrMoTW>FVMB;_UQo45**Bi@mP?pl!tWyE1AiYNLm))LSeKURguZ0mT0B zU;l28VN1l>a$x;Msoe$@|9nmG%4UXk(7Gd=9t1i?T_{v8=qOrw?-d&BN8k>9VULS` zKWqzjI^QTsl4oNBbR)Q8;#K-abcW?vUwfz8FDR6G{{-o`w+ev?PlYp*1exlZE?vVX zgaFc1J@-`gh(hMDK@?0%%^(J%&qiElQHPUnbFSqW^uDvmf{i@K%&3hl(jWHdRc5R{ znE~%^9hYwE+_v!HVI^NKpTjLh7eE!imbN5K4hb*3u-AaGKvDx9B}8lq?s0OWp`J(u zmw-?TJYBkBys5_WFz$~Ec$LCpr`qs1$$VbS3_HYwvRWLRN4mJmWu^_&D z=dz}t=Gdh@ked)_IY3*|_CP;|$agtfrrZL<1-m_Yjr7z3Wy}Kn$ zQrb&CS$>&DX12E3Dk$=}Su-4{XiEc8JQi0Iz zC95g>TDSe5~^K@5lrFc-9CXkUGPcc9uSzSC9Ar9&{t^kW#xcuDJ6W*I=jqC>4%53e5>_Muti_sJ$r$t_BZ6O=?t3Ur&L~v-SFztVI z)MZ^ zjWiZ^l*Co$Flzt`$>g{1L;!y`bIs-S=v;Na3_#Ci*Ic|;Lv!wJgGl^fn=A)u=+&Frk zSsy)fdvqKGNVtH3vk!pm&aMBoc5kv1Yj-qH|L553bBQ=RNS3da>O}WF!|=w@q4UTF z=GOJm>OjjqvO!HW=CIxF7LIxcKLMFZz;v+!idOtaO;*W4*ga~+)8AeRh4s4tsLF2N z^T9S~g1CaDlnRdUo|<65Pi)Dg{%HOAR%mg%Rv7X~*ya5tp{^AaSFH(&#V-$J;6~X6 zFL^cUnF$znMzc=-Cur)0Wc~SYS%P2NM+%G{dG056AMz@pVtSu%soVqA?Ltkh;k}J8 z1>f^vlOcVGjan@D5@R_w>lY+ZFF2?VaV}MoB%%KX5#{a@TWHhb!0UwySAj5>A8&}! zIVvv7C%ZMf&CkY%G|~eED%LMk@g3(HHu5j`MJ{}|wIVIu(L>%wH5ChRx8C|9d#Lk6 zw}b2I3s5favTuSS+e0M$-#p+9rB)SUR!(rAGrRw8ss)uC**OO)wZ9$XR&_r!YA#Q%+vr^}D}lfP0f1Fb)hFAJP6~$gQ~?U;mT`^z z$zp|7){C~Biz0CVqQS}j*-;zTW4ZFWZfkGr63e7a0tyygHwoof>4|4N0jKLPKo-Z* zu*xw(6%nqQZ{hx)fbb;p5P@Fw%lt|>00^1IZTwGrQZ|pT%e}ltU9L2K9TwXoWCG!w zQ~XuRk7WePBhuwUXcValz|=xtX*{iQ0hSNgba&j3aYy^Z5JwDfS8a?p0jAuJ*}zuf zI}2j2XvB)c!c`p@{osR)oWkNsJv%o{g={rn@cIAaE#;dI6utq z^>-=SdSYi*26G7Jlt!c1!5WVd4ep0=+Wy2P@|1MmCn21qeUDQjsaE~gHMD3TMXP_` z!xK%nPfs4erc5By(tY>4eFjvwx82eUZhEQL$fCB{>pK-?H&}J$|IK1@&j4&tjzgNC z3K^q0K^~@73I;G>aK?m#x(g{U8@-Wp`PT?kZv_P< zB;ocgW9nWHr{g|}qNO1iAc`onAd z8F#XC>)pE9*Oy^a)>U#Ny!8FW8nYxcM6dKR097Vx(#V7aMqo&mrb%}u`PCj>yfmdm z62PwF=jLh*Do}#)J7Lo!FR!xnSY|?zW!M&rS%`t_%S+^=ym6f13HPk#7~H@RXTuOn z6F%J`lA4d*H)*r`X}bCYtNa}Oh4J(k2iPBieHFn~^2IitJ6bhEBm76jZ z!PI6qNv5q3JvN6E5UN9#6{xEIEt$9tOGSLIRFx`is9=ul0wg)));&5tqYLEtQU7zq zw*2TW)g9HfZz%_w1=<;7>7$HKfkPLg6(aL2@0|X1$H4~7i5Iz_2TI`jvY|iSU@9|l zKJJP8+{p+82)Bk2gOsnE;=StU?ohL-v|cyZ3;QG8*`E&#(C1f&rg(t5`AmUZTUSuF z<{W`zE#NhFUOqSe8=OO&fRvU_t9+CG1KvXlf?)G;b=vIoaAKeV)pgrxcQteloYk{| z{~Eg8Yq$SYiyx#Ku7GViA}!g|#^)X#P662q_b=+<+MJSjCym%44241KEsxR?a`2Bm zS4yLR8lYPdcpx{A^5q#ilEOufubZ+?nDr}mf1c9ED?Ch|01`kNv=<5nK*eSrn;IVw zE`2(}$$lGN7QsJGPX8(X*l${^q&59;No@bCn6G3>1j+;@ht-bS)r?Vb9dz@Ci|^e_!|HX+5m;*ENJHiRT(we&xR z7Oz3CfW1XRA;2Mn9%q4;ZrfMyhJ~Ekb}36`$e5G1V?%2>a+enK^sN4v#0QO1{;k8Y z5Bh4@S*GTtQc*Jjnq9@L{{{)qoj87p1GNxVIFDb8m808Jv!C^=C7 zpSzN{zkhe`B&q32^WC5KZ=T79B#&OH`^fwruZVS=1Il5ETbam8_Qip zCx*$O3A4Zl-2g+} z*&-)}$y^)PKQ4x1qi{u}sF-QvWX1lb=;rgRcc+d2tRha6yX_uL>kP%rwKr2Dcr#_Q z1?FFl0NX$6)oRvj+%o_1d+e<9m1(1Kx4(Ue6@jozJvBh zH*$;;I^W@0^~$9D37?_gQ%uSE1pI`co1JP`?-`R?sSR5O zD~qHC|JqF`Hbt-`oDFVKfC{(~>c*otu0j35yn5g&kxsW6l~&Gxh+*m)Dew`DQ^eu* znof%dbBQ-JVEgqd{IE$Upr-99?*H?sO!^CMg*5gc6G;Ttru=F!mZFxo-J zzx1);*mI*4@Tp36B&{mXVXU4nv-%#FHYxDuy0j@xJfEiOUo>e5>H>G}(D@uk&Dn5i z0y@@cjt&&{gNhMsY)7;xJLWa$?lfzwelc2GDPGG0bM!0i<+#+Dw?0S7XE z=e%O?{Vyv(j{v5m^}DJIE0P0AS+4Ps2%-9dGWM6D@Z(3?<2b|EOadGok495LSxAPl zZrM;a0U?@Z=PE-mh-6Mg(xPTl!^R%>*5onaj z0JkMn##d6UZDrkJQ~8i?G|BFHwqs->=1O%JwobYTmzef}^l8ZlKY@>PS(bscXM^G46m2C-UH_dc#>j zv5G|xhKuuXn0C0+18Jd))yowDo+x40uSmDAh7BQ>a%utedDs3UmWOcDR@l(o_0?v? zbZ|zeZLbx$%x3UmySpuG%aa9mXum#de3_*CgJhG1sQs-zriJwiItfX9BT-Jw-$sxH z=t^>VhWuZ&Dk2na#VVOPlr9)!Rr{j?VF=xoG0||1RPQ&1)1aI76|}_=>(#;RoID|7 z>czamU`l{!e_p-hl6`3lt^VK>yuCU%O=5xL_)t%}av|lG?=j2KQU<5wp*VgLj4h@t zi}=X=QpemG@^#KP5buj2(O1n8?11YOALH1n|H&$9^cjUSkQTv^(x@v7W2T(YcJeFz z-<;Alk9y;C6w@BftKPp?vE}q{)F{K`GfC~juHFGhr{Q>rusVF5mEnL?nc~L%Cd|VT ziMeqMvmt3B+gye@V9KagQ$A3=AWW8x&dK2-&BN}B?>Yur(h#jPz?YGJ z67PALPdw8#N2wdbWfuT0OG4B#rHa0z5s#&?CAc75h_`md8Pj56HR!LXt1JHpiCDJT za7@7JbJX_!SmR0-U73jq8G|4nxbW;EyEhJS5F~h);7K{?a>7t z!QIQF%sVwSi8cJ7Ne-Z9D@yAh=XDfM#F-v@09|B@4gB@a4T@PGR98+U@f`m$Sf%%^ zFDxlVDw-f>lEYe<^v9)3qK-nb+Ff`W5v*U+>Y_#09iwa>aIT5_i3J_J<52!Ri=kEl$Y-G-D2LRCI~q z+5Owb`tRAB*Qmi}SbLy+u&?N=x<_z9eQi%zo^@{9ION9d}J;Z3H~0wd1dIDx5QWI182<-s2DFh_*g%FL+&S!*s3~KmLp^ z<-b|1=Z>7OPI(`;kkRg5K&bkf%n3!~KzI4lOd~C=Rct}aTRzg5b z;d>>m-RXlA#ZsulJ6xGGx1gA>wF%BaJpJ%0``hwdN~uTBazK?f%x08XT<3PLu;m&P zWr;5H_^LrP`nzUl7gIqtO~sPw8@LK6)u5@J%-%4)L=`N$%2l9MbzbvrgsInCfZ?zw z?xqEmcEf7=$A}nA)kEa9p@A{YF0c=BYV(tJbC2A*MemW6VJJr)LJ9_;K&BFjcb%Lr z>jD5AKGw=ZO<5M@ftI<2&tToO*e`lCf3JOr?Piqm`h!11%%d^bHrzzmRGf<3li$p* zNdta}es{FgN;Q&qiUUr~++7$RKi2~f@;E46>5sa6r!wP3lI_|@1ac&Ctr$Myt6U09 z7|R%{pP@Rs&b+Q!#qc%=jP7;+%|RV{{r=54AxF@*N$xG9*+EYX?}|;DK!3y7Z-&(S zRdG)mU8YLH29~@+#>MmkBJer2e!X;&UhJ|?A$aCOATYi1)GqJF?P3zyq*Q@YW0(7^ zuA2>U(>wVS5x8Yldzk5QY!r8ec6X7@Z>72_D$cDJOGr%ARwwDWQQ50szT0sCN{o+_ zX}K(9-8~_@5XE^b2X7Ubx?MV=jg4LFzLoib0@f!wSll&D`fXj{LKOHti#BTcqFR`L zm|%^4Pjy%z>i*yl&GsqWGUBA8KCE`y*DfF$^!|h^pOY({ zx8nE_zMW;LzlY5RuM&@AYyqEKS^n)vP~KpGr~pMS|qJh%Z1F%0c0ZeXJUSHvm$hBQ7A8XmYSVig}xzqikQ*ma)lAr zy-KIa=dTchzcQqB1k136-gsaAeAQb#P(Cv(S#ce(szZ$Q##7XK0okRxk1ZT#s%g@J z{|z`r+wE2wKR#1wsDM%4WWP6J^A_t=k}a7+XqPLv{Rnf^@jy`2akJ+<*0>;p63!yG)+Broasjbk<3|aG2wFghPLQC`lwA<6iy) zT+eoJws$^SGm$h_jKrtd5zQktSIWOJ@1miHhlKaBGw6TxO)@zq^Af&0KmTlyeiQ@X z@^5`FwyD=OST>e1>VJTZln#d%Ai?j67K zV4MT!M9EY8n$O-wTjnbI)!pWwq@7ljWbu0ZIBpC3H<<=fjq{5?P%6t5*Pv#D;)mQ| zj0vq?u4$+u!eEHvREpplVmkNsSVzr}n=-17CFRhW9f|KjiLOryOkO2hJxiCgnp%dP zg0FQw&z1hrnVGQtou6MsZ1UjWK`7YQEs!+Xs~q5ywB~*^Yc8lr4lXb>FwcNz`@1a)kgvJB936t#BtIvV2OHzBot6QZ z5Zis&F}ta|U;q_&1ZRS<4;3yvPq*CYaS1Gu(6CpaKB>7Q9ce6|pCrBU)CZfi1VgX( zgnXaHfJ@Zx=?xwB=)P#&vxZ9?yBdX3PQ*v3t;Vk6Ba7J;M;ALJWS#9<Bw+1yQ+QI>lJl8lKP(22pR90e2?RR8Dd!R`mOtbuDah}O> z@h5TZzGGGr*w1SxW~UY6c;PET1&y~|EG#$lONQndaUhXOvR&G?L;Iy)v#GDC?=xg; z6W%kM>j>iDTb5y0ngNc*5n)S-WrW|32S$@-Ic=06n44!!IDwjmP6lUd3-H!DJt>8{ z#FR#0G@pf}G|MxV@bsDF@p)t)WyB(rkUj|H8j5gA)Y}4Wr3;?k2_9h>|3f9UCGYwP zvRFEqciXkp2mN=e>A4Yvb9IR+X^pU5a%=>M?{CIb`6`Q2CCIbFu+|kL@tJkVVb>{R z518mQyazbw?36ulOB=j*#PvtE2Xks&cM2z6S5X4zLo{_qTkpZL}SP%NBc7gA*K1$Egi_Ss*P}aa8ZDa+i!n@YD( z@8f##eHyv179N*AVSpJ6#t~YLxGR88!sB{AcNO$rQXRud5yTT6ug43lvZS{;z1vZ8 z;8=76g~LvG;UfGH%FIzJQniZs%6}Oh6e+;ml8pKpJT=voq(GJ&KV<`f6B^RK9!Cxk zWI0BI%?7wd(gV0o{PMb}<^5gSj-66rqGlJF^Ejo&miP*N%F1{2-v><-eQj}|28^Ci zZ?%MZQ42PUsjmg)Lo>8sv0|q64y{B#L?J8)RuW}k9uwW|ov+@h6rT8E1u2vcPtAZN#TzVc@XoNQ`QH9luTWjwy%zX?ZCU%aO9C3-)skLq}NhB3Gq@?|I+ z*Rqh&tXGDAUH4%Tn%yFxWTj)q;_j9NI-d|j(&6>q!L0X#3p5wVatIrs5$P;i#(GJb zQ^q*$772s2<)zam31ITo+k0#@4m4tCc2F$Q>Y%*6tczzKhP14jgciw17wgd(eks1` zUVp^pU4-mrB9%b@U1l+a*D_hInrm?(X)@BwW+0v%cQ|!#fJS?iI zVl-wFE3Us8<#QbyOYjnk=~!sJa>aH+ck-ziL1H1-}){n?GKU5em!}|gU;j1w}u8k$}XK*@Vxk+leIUU;!E*~cbrc|r9T?WC;8-6jsqg+AM zffC+(3tN!O{#n8xcd_Xf9EvLW6*QVe?GoOx3Vo>PLWkI0MRCf^`ay9eQT9oTB>8SR zqxhz8q%>kQGLPj3KBTvJAhek2*X+!V68+z& zYhDizXr0GKdooYD3zYSiE1vxar;m_;7rrQbJFHx4qQ~0R|DR+h!LHPJtH%>ILV|htrj~?cty5)H!fuf#eZ%8gBem!2b1*BqfOGfwfiD*S z-?fU8H5^+6Nn_DzQnd5WNQN&dkG#ZH8Ej5W29O<NF67Wx$7!q1WoK812PFStPS&GkK~KiIqtB%<#D1e+F1<; znsW@zuBUlZZ>$cL+5drmWIC=mm-t~-+(T?t_z>~xi`?bMr*O(B?a>5GXsa*_nan8h zHUQVYK=b&Z7S1rjwJfyr*{3VjZCe0? zkYc&g%ASyT&fg2M^d6iNeCyi*g4_ddyQTXC-UXgB8K~tNGb$I-)t2kulGIEF?Irsp zXB_(`1nC}ZH@HLtTjZRr{RvJYGjmQ(rjHAKLkl8*RnIfX7UfgylRY|^|0b#bk+7n#u6f1B+( z0B-~!s9gfAd?>qyOUHzT$A(!B-?-ZhroON*Y$|-VovIU0k0EFb+>XG`cDJEv zMA)7AN={~1xSy}1SRiZ6OBb8@EN>m1;Oigj=%yi$=ZTwjHv=w)Cpncm3j9R0{^*3?~yThwCSCNBIj1; zw#>mE*gf=!9~A742BCc}y^1y$uq#xXqH?VizD6pSVo>mVkqP8USL90gxFRDPvr|nS zmZ0afS3kiihN@>GF6AGXa+^Wi8`%T_9wQ8qU8iP8HH_efXEDSSxD#U{RN^NnZgPKRw(VA3r z@M*1l=O0^$d7^l6tMYnDad-B8NDA}pVuq8`OKyw5-wW;uO*C9~r9m#aX-6P7&HDc! zGJ)UOXL#y<^tpY_BQEiBxbkl2y|IY>`Ll(+SU83C%0){FTk8!u6eBp!d)G-;v=ugG zlM>~063=sn))cgop<_zejjde&yTgJ4mNpNyFN#$B$qn#Uc2?~n{!{760mmi$74}yp z^ufs+$NGZrJE(x-T!m}meix0q1I~%yRQ*4)Xlw$ycXCJB&77<8aV0O5qyuKmL?~Fs z9D!qC@9Ok8K6s={a>G~kHix+B`a5kA81=lxfC&?e58dKfrgH1Iwc;L669eMU2R(vX zc{9s~HT)qbfSTg<^9{bbO`6?!%rWPV2Sw1`Q84(2YVY*;fYHbGg&GVTBIPf<(@gC*~J+piuO2J=# zHNaNvfGOpfaM4QI*EX+F_)7nvq8zos{P9vApRxoNp>rQiA~8kLm8=BiN9;V4{|a9C zyXDm#Y>G;gYzLEonaR?j8!vD@zM{V@7lj8R{Wjltc z`xKScBA%nrLJqhJ=|)16*;wsh=gIw;E<-q$dt3OSX_1WOjgC z>%z=986}^w6bwDhzjyN&>Y$65s}TrSqK8?@8!!+A601t4HXBy#jn{dyGc#^TfOQJ2 zI=Kk_?-YtnJjBE1sJo-|Yw1jek&J5X+)%2hj|cFuuHOz65D(Cg!ZyWyX= z&g+WIEhYZm2tC9T(ADxGuU#7Wku8sm| ztIqIIgLNW8BY~*e!1=Ru6}kD2x{GWQWzWspaENoIaH<@Q?=6>61`RXCM>_9(V2ylT zHQkoKZC*>+{GLnVexp5=CT$zjeJwc;8fJ!(6Gnwdzp-3{Aol3sb|)=nAYb~Hf-$Xp zhmj;5%WSQw9k;T(DR)8Ew3=gNHjm~%>hXU_S;!7WKSZ9rwc`-McQ=QsXISnUCVlQ( zq-^oW(4We1I)vCD@3ciE*1zn{*|iXwU)WEuv^e5p48uJ(U;|M`3-bDXrdMN3&>oh) zhkk`2$Q={JY?W`v?gUI}Ely)jZ7vc{Sa6mMhrk7ze9lc?qy}@jjBIn@znHQHB>*O5 z$p?u@^27nd+e#TQc^!#*kEMvszta-ql5d0O0KMw2&5_pc&MM^jSr3~13JBvXa^tmt zvShAjrvCvP#+Dgv5a-x;yo@!KR0;8j+_wSV8A~Zpph8qPwx6*v*U67UK(vGRL*>4# zY)Lb6;@!8yh_Pu&isR%(IRTtCpk{pd6Yxw%C~+#S900yW-6cPIeyX{h++8!vSk)zEus=X<(cG?`4Q3$(^@}rxsK)c?n z2#vxi?(#V@h#Hx370b|ik@tCn`Q5DqI;#FLr5rj%tqzBU?fLCdWBx|0={+@+-6y}* z923A#NoW5~pkx%Y0WPir+dvThVQKIq=!bFEG$=QbIbPuziVMp82@w}Le7cK7}WZ{S|Xs@RcPK0RrDio1_{LQFbdeF^zc{wFaWmA zR@^=d@~dL7qt9rjvN(97lXI3qNa!90O=u`1VwG;h;81Fq!J7cDE6|Q|OI(^GDZpr;b$z;lThL+I41#2qa>9VVPn_sZRfl z>lx6zQ_#?Vjl|IvsT=xhF~7`bZ!#bFT7+;GneIO|D1*Sf5&Oi`GtffWiYaL$@)Hs; zCtR8`G5V1O<+^)KDZy%V&DyGWP$dEUbi~W_3_&6rK_aUD$E8_iW9%G()S=KgvMip^ z;*+51e`w>ld^#Ot6P7s;hbCVR!jpg0TQd66F)>koMoCm(-MUn=O><(?g(Lv(aANTB zjQ+(J0^@PDc13waac66eQE{z)p*oZv*+Ja>Q+I!?*$}#JMh;JJ0Eq(~Ozo;E{9U zbHTtcFuXs*qZr+*is(_?;)fRVbOc-pTH_E^&->dg=_ zE?n(f$f(tHUE&BKDK$Ip^p+PHl*aHwcFAS+UU*FLH(XRoC7hPFXu&8+wBm??3PtiK z4`*9nE7f#l#+gRGVHbV?8g@uR=Kr8&TQ4&HR+n!?qQ2xYF%o-4C4N+7YG6+DBoz5C zYLU(^du+!9X5Hd#(pp#j;aP(SHVzR6f)WR~RXwzPrYy+tv(2A9^>?f%mpDZi>$HhO zK-k=w$MF?erZS}^lvU?6Y9;T1$<7V0Lk+F!2#+3XC~?HMNlggr5{-qgjphG8O75J0 z*$ao6-&3DFKyTY5rMP`FfO|Fi9TOG>9vZj^@xb`3!c{IWSwTKYy?X1AQ8kwY(bW*_i8 zdtZQ>gPH4qSO$b$JQpgz4KBsv$$PkbvONrQr)^#3&4i0NB8xC!jHRST)05~8X9<}e zAaa8NJBbRi##*moM6_d|MgAlvZXaAVZnk2rH;y+{DzoS2659X%}jjsa&BG>U|+U_#Tgh zVzW5;P2uw_wC%0xAriNp&Evr-XWz|oN!#ncqby6?<;nRcq>JBE1 zt7e`fk6wplw!#?^rvqh1tfQKv>yr>=<}zFC!bE_u0(ZZ8E+HfwWWU^AB~H*)#1#Dn zHBg!I)+=m{uZ9TIrsSV2@q&zQLrEtfOE{sRZr)tza;rt)YKr=96t>COYAQ65c#6p5 z1vwPt+OMsmUQR#}QjUL?&Gn|NLdInipxX6(&j%jRkngsz{pE>^%M@NJQF}^(Bwcl! zO@f9`(J10DV8)>TXD#rKv@1$BWd%`p-@Y|lhgjl`jbD(%=ob8HgxEJP3|ncT%yF-y zT>xA*Xx&%|yB$(E{pHALfEFGViFfzEidgMa>wROKPa1?=(fxnJHMIVu<)Deu?)JK! zb8fLBfTAbxWZY@yT~&X3t~m5$Y6Va|tbA(r?HG=%=?(;R3^k^nl=Lqn^b=Wdg#Fd3 zH4zo2^Y+Cba3xalw?@w4U*+wz+x6LhGj2@Rx7FFvlxJWBOQ4@Z`$h3O_0&9(L=_Nn z93iLTT+P1(`eyxuN(v(^m*DQpkZb_g;#~8gp3)I0(e| zSja(q(AbnfYO&)I9*8-MizzVS5Vw1~`V0iPkqz@!%P*>!UA59)>)brf?6c7H98T$O0@#KIw@MgiHzFnJWUQ0n?Lf01gs245#}0d zj}*_Jz`_jR?_@{a-2{YDDBV|i!c2em0Iyt75}n+9C_a4fJWmqFpB=R3z>I)`o>`$I zSpI)hjR1l$QoF-}AWhwq!C!9ioGwq{(oUjz0vY@%Dj?CdEPp3Ju|q0dle-fasYPWI)MRu(*I+S_70S$TsK zZ4wfOZB9=6p~ELos_L4zl+- z^6OfX5p`bBoi|XlbXgzmtg!vH5Cj6V?)*mKdkWjM*?`yxn(Y<~dw7Scu5K0rWPozg zOr{!yon^FJM3l1;B}@_Dv4n^Ekx85-#1uje)M&oN`<4%h^zEX*jKklv(>VbpGIZ+@ z_}<|wD%@U?Kk&pS=i;@2gxE-&iz8QcCxhWU2cTox(kgXj#GXl z!D;14I{=B;?r3@P_5CXC@J`i<`tlUf<9TKuy!4x0QtOqi!F--!^Dz@_Yr8bvF@2Ax zj|M`7po2L!!L7f_9OW)|L|i@1_^5!d16o^OU-hSq0f2Gvs|B@PCj}gR?znGIqCoVx zWZR_rh~{o_A(yJ0#0Xga!s{qKy3HgkgGE}ZZu zv3ARGtv}E_L_2omsg-LXt zd~XVFiE0PRdxF7?sJUIcLWa42Q7?pen@R0P^-*4ljyR#as#=VAUFXHIz6bpsM!c)W z9MG5-%3H4S$dV<7^PBZ&uFbDtC4oH-DD(#rIu>t0<%%zJyi+JtBj@J)@^T;OU^uDc zA;FgM%@;5u#J=GP_;)l8TrC-&lcAk?;%0WtG^fP>c=CXqJZ8gf6J9M_Pk^Im*agX+ zLK8OyO2EsC%9ht7e0F>bzky5xc=0PQ7HI!X*zh%wd1TyWz)^wL*!0Fq_(rxCSb%re;Iw ZBeU9xyG!b;4!_F()_c64$x=E1006MH*&+Y{ literal 11284 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 Date: Sun, 19 Jul 2026 04:36:29 +0300 Subject: [PATCH 17/19] 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. --- ds4-color/README.md | 7 +++++-- ds4-color/plugin.toml | 4 ++-- ds4-color/service.luau | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ds4-color/README.md b/ds4-color/README.md index 9c8bec2..73792a6 100644 --- a/ds4-color/README.md +++ b/ds4-color/README.md @@ -18,8 +18,11 @@ library is required. The plugin writes the HID output report straight to ## Requirements -No external dependencies. The plugin needs read/write access to the DualShock 4 -`/dev/hidrawN` node. +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. diff --git a/ds4-color/plugin.toml b/ds4-color/plugin.toml index d8c1db1..015680d 100644 --- a/ds4-color/plugin.toml +++ b/ds4-color/plugin.toml @@ -5,9 +5,9 @@ plugin_api = 3 author = "hy4ri" license = "MIT" icon = "device-gamepad-2" -description = "Set the DualShock 4 lightbar colour from a bar button and panel." +description = "Set the DualShock 4 lightbar colour from a bar button and panel (requires python3 for the hidraw write fallback)." tags = ["gaming", "hardware", "utility", "bar", "panel"] -dependencies = [] +dependencies = ["python3"] [[widget]] id = "widget" diff --git a/ds4-color/service.luau b/ds4-color/service.luau index 3be85e1..47c5025 100644 --- a/ds4-color/service.luau +++ b/ds4-color/service.luau @@ -173,8 +173,8 @@ local function detectControllers() end -- Write the report to the hidraw node. Noctalia's writeFile is file-oriented and --- may not open a char device O_WRONLY, so if it fails we fall back to a --- python3 one-liner that does a true binary O_WRONLY write (what hidraw needs). +-- may not open a char device O_WRONLY. If the direct write fails we fall back to +-- python3 (declared in dependencies) which does a true binary O_WRONLY write. local function setLightbar(dev, r, g, b) local report = (dev.bus_type == BUS_BT) and buildBtReport(r, g, b) From 01bc188fb6b6e95c11c8f998bed832887c833f9d Mon Sep 17 00:00:00 2001 From: M57 Date: Sun, 19 Jul 2026 04:39:30 +0300 Subject: [PATCH 18/19] 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. --- ds4-color/panel.luau | 17 +++++++++++++---- ds4-color/service.luau | 22 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/ds4-color/panel.luau b/ds4-color/panel.luau index bfdbf5b..f9e5430 100644 --- a/ds4-color/panel.luau +++ b/ds4-color/panel.luau @@ -4,6 +4,12 @@ local tr = noctalia.tr("ds4_color") local SERVICE = "hy4ri/ds4-color:service" +-- Validate a color is exactly 6 hex chars (no metachars / quote-breakers can +-- survive this, so it is safe to interpolate into the runAsync shell string). +local function isHex6(s) + return type(s) == "string" and s:match("^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$") ~= nil +end + local function sendToService(event, payload) local cmd = "noctalia msg plugin " .. SERVICE .. " all " .. event if payload ~= nil then cmd = cmd .. " '" .. payload .. "'" end @@ -82,16 +88,19 @@ end --==== callbacks ==== function onApply() - sendToService("apply", "#" .. currentColor()) + local c = currentColor() + if isHex6(c) then sendToService("apply", c) end end function onSave() - sendToService("save", "#" .. currentColor()) + local c = currentColor() + if isHex6(c) then sendToService("save", c) end end function onSubmitHex(text) - local hex = text:match("^#?(%x%x%x%x%x%x)$") or text:match("^0x(%x%x%x%x%x%x)$") - if hex == nil then + local hex = text:match("^#?([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])$") + or text:match("^0x([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])$") + if hex == nil or not isHex6(hex) then noctalia.notifyError(tr, noctalia.tr("bad_color")) return end diff --git a/ds4-color/service.luau b/ds4-color/service.luau index 47c5025..fac3b22 100644 --- a/ds4-color/service.luau +++ b/ds4-color/service.luau @@ -5,6 +5,13 @@ -- No external binary needed — writes the HID output report to /dev/hidrawN. local tr = noctalia.tr("ds4_color") +-- Validate a color is exactly 6 hex chars. With this guarantee, no value that +-- reaches runAsync (via the IPC command string) can contain a quote-breaking or +-- shell metacharacter, so the single-quote interpolation is safe. +local function isHex6(s) + return type(s) == "string" and s:match("^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$") ~= nil +end + -- pluginDataDir() may return nil (state home unset), so resolve it lazily and -- guard every use. Computing it at module top-level and concatenating would -- throw on nil and kill the whole service entry (no onIpc -> IPC is a no-op). @@ -204,6 +211,7 @@ local function loadLast() if raw == nil or raw == "" then return "990000" end local decoded = noctalia.json.decode(raw) if type(decoded) ~= "table" or type(decoded.hex) ~= "string" then return "990000" end + if not isHex6(decoded.hex) then return "990000" end return decoded.hex end @@ -223,7 +231,7 @@ end -- Apply a hex color to every connected DS4. local function apply(hex) - if hex == nil or hex == "" then return end + if hex == nil or not isHex6(hex) then return end local r, g, b = hexToRgb(hex) local devs = detectControllers() if #devs == 0 then @@ -248,7 +256,7 @@ end -- Persist the chosen color WITHOUT touching the controller. local function save(hex) - if hex == nil or hex == "" then return end + if hex == nil or not isHex6(hex) then return end saveLast(hex) noctalia.state.set("lastColor", hex) noctalia.notify(tr, noctalia.tr("saved", { color = hex })) @@ -256,9 +264,15 @@ end function onIpc(event, payload) if event == "apply" and payload ~= nil then - apply(payload:match("^#?(%x%x%x%x%x%x)$") or payload:match("^0x(%x%x%x%x%x%x)$") or "") + local hex = payload:match("^#?([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])$") + or payload:match("^0x([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])$") + or "" + apply(hex) elseif event == "save" and payload ~= nil then - save(payload:match("^#?(%x%x%x%x%x%x)$") or payload:match("^0x(%x%x%x%x%x%x)$") or "") + local hex = payload:match("^#?([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])$") + or payload:match("^0x([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])$") + or "" + save(hex) elseif event == "get-presets" then noctalia.state.set("presets", PRESETS) end From 8440b0e711c0c3b8bbd05f87efb4b11f78ca19b9 Mon Sep 17 00:00:00 2001 From: Lemmy Date: Sun, 19 Jul 2026 08:39:24 -0400 Subject: [PATCH 19/19] Update plugin description for clarity Removed mention of 'hidraw write fallback' from the description. --- ds4-color/plugin.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ds4-color/plugin.toml b/ds4-color/plugin.toml index 015680d..f241eec 100644 --- a/ds4-color/plugin.toml +++ b/ds4-color/plugin.toml @@ -5,7 +5,7 @@ plugin_api = 3 author = "hy4ri" license = "MIT" icon = "device-gamepad-2" -description = "Set the DualShock 4 lightbar colour from a bar button and panel (requires python3 for the hidraw write fallback)." +description = "Set the DualShock 4 lightbar colour from a bar button and panel." tags = ["gaming", "hardware", "utility", "bar", "panel"] dependencies = ["python3"]