From 5fb0e0f279bad9bd5e1643840649ece7c7b6a6c7 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:39:42 +0300 Subject: [PATCH] Add DS4 Color plugin (Hy4ri/ds4-color) (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add PS4 Colors plugin (DS4 lightbar control) Wraps the ps4-colors CLI (github.com/Hy4ri/ps4-colors) as a Noctalia plugin: bar button + color panel (native picker, hex field, presets) that sets the DualShock 4 lightbar via the ps4-colors binary. NOTE: thumbnail.webp still pending (generated in noctalia thumbnail generator) — CI will flag the missing asset until added. * fix: add PS4 Colors thumbnail (solid crimson card) 960x540 WebP, deep crimson (#990000) to match the plugin accent. * feat: left-click applies saved color, panel adds Save button - widget: left-click applies the saved color via service; right-click opens panel - service: add 'save' IPC (persist without touching controller) - panel: Save (persist) + Apply (set now) buttons - translations: add 'save' key * refactor: implement DS4 protocol in pure Lua (drop C dependency) The plugin now writes the HID output report to /dev/hidrawN directly via noctalia.writeFile. USB report 0x05 (32B) and Bluetooth report 0x11 (78B) with CRC32 are built and the CRC verified against the kernel algorithm. Device detection scans /sys/class/hidraw uevent for Sony DS4 PIDs. No external binary or library required. * fix: panel renders black box (ipairs(nil) at script load) Move preset-click handler registration into onOpen (after presets are loaded) instead of module top-level, where ipairs(nil) threw and aborted the whole panel entry. Guard presets with 'or {}'. Replace unsupported width='100%' string with a numeric width on the swatch. * fix: match color_picker panel schema exactly (drop invented props) Remove border='primary' (unknown value -> whole tree abort) and wrap=true. Swatch now uses border='outline' + borderWidth to show selection, mirroring oldirtty/color_picker. Aligns every ui.* node with a known-good panel. * fix: service dead on load (pluginDataDir nil concat) + real device write - Resolve pluginDataDir lazily + nil-guard lastFile() so the service entry actually loads (top-level concat on nil threw -> no onIpc -> no apply -> no notification, which is why the color never changed). - setLightbar tries noctalia.writeFile, falls back to a python3 O_WRONLY binary write for the /dev/hidrawN char device (writeFile is file-oriented). * fix: replace Luau bitwise ops (&/|/~/>>) with math-only helpers Noctalia's Lua is strict (PUC-Rio semantic), not Luau: '&', '|', '~', '>>' are syntax errors -> service failed to load at crc32_init -> no apply. Added band/rshift/bxor/bnot32 math helpers; CRC32 + BT report now use them and still produce the verified CRC 523bce6f. * fix: panel black box (readonly _G) — use static onPreset1..9 globals Noctalia's _G is immutable at runtime, so assigning _G['onPreset'..i] in onOpen threw 'attempt to modify a readonly table' and aborted the panel. Presets are a fixed list, so define onPreset1..9 as plain module globals calling a shared applyPreset(i). Remove dead makePresetHandler. * fix: drop unsupported textAlign prop on ui.input (ui-tree warning) 'textAlign' is a label prop, not an input prop; Noctalia ignored it with a [WRN] ui-tree log line. Input defaults to left align anyway. * fix: use device-gamepad-2 icon for catalog + bar glyph 'controller' glyph was missing; swap to device-gamepad-2 for both the plugin catalog icon and the bar widget default glyph. * rename: PS4 Colors -> DS4 Color (id Hy4ri/ds4-color, dir ds4-color/) Rename plugin id, display name, bar glyph default, translation key (ps4_colors -> ds4_color) and all internal IPC references. Drop the stale ps4_colors_missing string (plugin has no external binary dependency). Keep accurate comments crediting the original ps4-colors C source. * ui: shorten panel (height 520->400, tighter gaps/padding, smaller swatch) Save/Apply buttons were already the last row (bottom of the column); this just compacts the overall panel height. * art: use panel screenshot as thumbnail (960x540 webp, crimson bg) * fix: lowercase author in id (CI validate: author must match ^[a-z0-9][a-z0-9._-]*$) 'Hy4ri' -> 'hy4ri' in id, author, and all internal IPC references. * fix: thumbnail * fix(review): declare python3 dependency + document hidraw fallback Reviewer flagged: plugin declared dependencies=[] but the hidraw write fallback invokes python3, so systems without Python cannot apply the color if the direct writeFile fails. Declare 'python3' in dependencies, note it in the manifest description, and document the requirement in README Requirements. * security(review): validate color as 6-hex at every trust boundary Reviewer flagged: last.json value entered shared state unvalidated, then got single-quote-interpolated into /bin/sh -c via runAsync -> shell injection. - Add isHex6() (exactly ^[0-9a-fA-F]{6}$) and enforce it at: loadLast (poisoned last.json), onSubmitHex, onApply/onSave (from state), onIpc apply/save, and the apply()/save() service funcs. - Since only 6 hex chars can reach runAsync, the single-quote interpolation is provably safe (no quote-breaker / metachar can survive). - Also fixes a latent bug: the old %x regex captured only 5 hex, rejecting valid 6-char colors like 990000. * Update plugin description for clarity Removed mention of 'hidraw write fallback' from the description. --------- Co-authored-by: M57 Co-authored-by: Lemmy --- ds4-color/README.md | 87 ++++++++++ ds4-color/panel.luau | 153 ++++++++++++++++++ ds4-color/plugin.toml | 33 ++++ ds4-color/service.luau | 286 +++++++++++++++++++++++++++++++++ ds4-color/thumbnail.webp | Bin 0 -> 43682 bytes ds4-color/translations/en.json | 17 ++ ds4-color/widget.luau | 21 +++ 7 files changed, 597 insertions(+) create mode 100644 ds4-color/README.md create mode 100644 ds4-color/panel.luau create mode 100644 ds4-color/plugin.toml create mode 100644 ds4-color/service.luau create mode 100644 ds4-color/thumbnail.webp create mode 100644 ds4-color/translations/en.json create mode 100644 ds4-color/widget.luau diff --git a/ds4-color/README.md b/ds4-color/README.md new file mode 100644 index 0000000..73792a6 --- /dev/null +++ b/ds4-color/README.md @@ -0,0 +1,87 @@ +# DS4 Color + +Set the LED lightbar colour on a connected PlayStation 4 DualShock 4 controller +from a Noctalia bar button and a color panel. Click the bar icon to apply your +saved colour, or right-click to open the panel and pick a new one. + +The entire DualShock 4 output-report protocol (USB report `0x05`, Bluetooth +report `0x11` with CRC32) is implemented in **pure Lua** — no external binary or +library is required. The plugin writes the HID output report straight to +`/dev/hidrawN`. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `hy4ri/ds4-color` | +| Entries | Bar widget: `widget`; panel: `panel`; service: `service` | + +## Requirements + +Requires `python3` (declared in `dependencies`): if Noctalia's `writeFile` cannot +open the `/dev/hidrawN` node `O_WRONLY`, the plugin falls back to a `python3` +one-liner that performs the raw binary write. Systems without Python present cannot +apply the colour when the direct write path fails. The plugin also needs +read/write access to the DualShock 4 `/dev/hidrawN` node. + +On most modern Linux desktops with `systemd-logind` and `uaccess`, the device +node is automatically accessible when you are logged in at the seat. + +On headless systems or systems without `uaccess`, create a udev rule: + +```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 +``` + +Re-login or run `udevadm trigger` for the change to take effect. + +## Usage + +**Left-click** the **DS4 Color** bar button to instantly apply the last saved +colour to every connected DualShock 4. **Right-click** the bar button (or run the +command below) to open the panel: + +```sh +noctalia msg panel-toggle hy4ri/ds4-color:panel +``` + +In the panel: pick a colour with the native picker, type a hex value +(`RRGGBB` / `#RRGGBB` / `0xRRGGBB`), or tap a preset. **Save** persists the +chosen colour (so left-click reapplies it later) without touching the +controller; **Apply** sets the lightbar immediately and also saves it. The saved +colour survives restarts. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `glyph` | `glyph` | `device-gamepad-2` | Icon shown on the bar button. | + +## IPC + +```sh +noctalia msg plugin hy4ri/ds4-color:service all apply +noctalia msg plugin hy4ri/ds4-color:service all save +``` + +## Notes + +- Implements the DualShock 4 HID output report protocol directly in Lua + (derived from the Linux kernel `drivers/hid/hid-playstation.c`): USB report + id `0x05` (32 bytes) and Bluetooth report id `0x11` (78 bytes) with the + required CRC32 checksum. No kernel drivers or external binaries. +- Detects all connected DS4 controllers (USB `0x03` and Bluetooth `0x05` bus) + by scanning `/sys/class/hidraw/*/device/uevent` for Sony VID `054c` and + product IDs `05c4` / `09cc` / `0ba0`. +- The last chosen colour is persisted to the plugin data directory + (`last.json`) and restored on next open. Left-clicking the bar button applies + the saved colour; the panel's **Save** button updates it without writing to + the controller. +- No network access. Only the locally detected DualShock 4 controllers are + touched. diff --git a/ds4-color/panel.luau b/ds4-color/panel.luau new file mode 100644 index 0000000..f9e5430 --- /dev/null +++ b/ds4-color/panel.luau @@ -0,0 +1,153 @@ +--!nonstrict +--!nocheck +--!nolint UnknownGlobal +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 + 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 + local selected = (p.hex:lower() == color:lower()) + table.insert(presetRow, ui.box({ + 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 = 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" }), + }), + + -- Live swatch preview + ui.box({ + width = 200, + height = 72, + fill = "#" .. color, + radius = 12, + border = "outline", + borderWidth = 2, + onClick = "onNativePicker", + }), + + -- Hex field + native picker button + ui.row({ gap = 8, align = "center" }, { + ui.input({ + key = "hex-input", + value = "#" .. color, + 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" }, presetRow), + + 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 + +--==== callbacks ==== +function onApply() + local c = currentColor() + if isHex6(c) then sendToService("apply", c) end +end + +function onSave() + local c = currentColor() + if isHex6(c) then sendToService("save", c) end +end + +function onSubmitHex(text) + 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 + 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() +end + +-- 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 {} + 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 diff --git a/ds4-color/plugin.toml b/ds4-color/plugin.toml new file mode 100644 index 0000000..f241eec --- /dev/null +++ b/ds4-color/plugin.toml @@ -0,0 +1,33 @@ +id = "hy4ri/ds4-color" +name = "DS4 Color" +version = "0.1.0" +plugin_api = 3 +author = "hy4ri" +license = "MIT" +icon = "device-gamepad-2" +description = "Set the DualShock 4 lightbar colour from a bar button and panel." +tags = ["gaming", "hardware", "utility", "bar", "panel"] +dependencies = ["python3"] + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + description_key = "settings.glyph.description" + default = "device-gamepad-2" + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 420 +height = 400 +placement = "floating" +position = "center" + +[[service]] +id = "service" +entry = "service.luau" diff --git a/ds4-color/service.luau b/ds4-color/service.luau new file mode 100644 index 0000000..fac3b22 --- /dev/null +++ b/ds4-color/service.luau @@ -0,0 +1,286 @@ +--!nonstrict +--!nocheck +--!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("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). +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 +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" }, + { 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" }, +} + +--==== 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() + crc32_table = {} + for i = 0, 255 do + local crc = i + for _ = 1, 8 do + if band(crc, 1) ~= 0 then + crc = bxor(rshift(crc, 1), 0xEDB88320) + else + crc = rshift(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 = bxor(crc32_table[band(bxor(crc, b), 0xFF)], rshift(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(band(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 = 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 + +--==== 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), band(tonumber(vendor, 16), 0xFFFF), band(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 + +-- Write the report to the hidraw node. Noctalia's writeFile is file-oriented and +-- 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) + 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) + 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 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 + if not isHex6(decoded.hex) then return "990000" end + return decoded.hex +end + +local function saveLast(hex) + 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(path, encoded) end +end + +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 not isHex6(hex) then return end + local r, g, b = hexToRgb(hex) + local devs = detectControllers() + if #devs == 0 then + noctalia.notifyError(tr, noctalia.tr("no_controller")) + return + end + 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") .. " (" .. dev.path .. ")") + 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. +local function save(hex) + 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 })) +end + +function onIpc(event, payload) + if event == "apply" and payload ~= nil then + 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 + 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 +end + +function load() + noctalia.state.set("lastColor", loadLast()) + noctalia.state.set("presets", PRESETS) +end + +load() diff --git a/ds4-color/thumbnail.webp b/ds4-color/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..7fc8b231e90cb2b005078c46d18420d2b0c9a3df GIT binary patch literal 43682 zcmWIYbaR`vih&{A)hQq>z#@W+fq}t*fq`K^GlMh}gQv4|zzIeM1_lPk6CjesfPo=5 zucRo*-AN%LGD?B{0RtlgSe}6)F}b(^qy`6OWMKGq4Wjn4E)F&LggH}Ei<21`7(OsC zFsKwoL`E?%Fp4lRFo>i>*cu>q5`=97VwaZ`lrS(b27uT_AwkXz42&rZ3=A6SNbDpe zc5*=xNPQ0j1A|R&PASMPkb6MB<4Mg64Q60q;9y{2kYOlh2x4$&a02-sq=11Te-Q(N z#a9Lf<_QR~q?rs1+glkJByJ(ZR2DEW@V789Y&(w-Lxf2|Vo@U4JQfB92Bx$$28K_| z85npY85o2wFfedMgXI_)7;v~v0qiz+hJ1#6hIED;hExUx22X}OhGYgk1_cHq1_K5| z1~W&W0FP!+Y^p1W`TPmAR-WPJ(!#t<>1KtBkmLWNW7plj)H7L~w|!Xu?>X(z|JT>c zbs53}EEE)0FfdHuW17pTb%0rh$&Qn$(V!<$v{6GKI_doqV??2yPYFAzF z|6lUoOW7_V2Fm`ak`j^xyCk{Gac?s<)kAasQKT|NhsS6?{D0-Y(|@o3nZGdpdHl8db9J%*{?%{&!|?y;E5`rE|Nn1be|LSW z{e=JT|4sbS|2y%k_LKP!{s;Z>`gi=_=0E1g>U(PYzrH`pe{25d`OP;L|K87B-(Oq! z%lzB^tM}jR|MdS@oy6}4f0lhozb5}E{)zex_eb_e>*xJ1{~P}A`hW31#~&=4VDsng z^?&gJ{h|B(|91br|HJ;>{hRyW)zAN<@pJ#@`4i>-{}2EB`_KNz>F>8c;QzS(iTu_7 zd;b^y=l;9>fBx3_llX7{zgmCkzwl4vZ>)dfe{TOi|E>J9{pbH@{r~#i@L&0_{!jN` z*x&u1_y6Z-<1h2y|9??`tbWn|`hV>Imp{<|82>c>*8eU4`~TbiJ^%mw@AW(P|Ehbm zKh3t|f7t#Tb(wYl|NrMR(un3O%hD5MoE5O`L^kWPYrW=QL;bd1dzfi`I&Is;jSHUk zhAzLmc}M-X_jlLGF39)iHh#TbTw}H9=D*G>UQbnTJ~Q#R(z=hrs{3X+O)V8&<6V{h zr*&M^-7>MjxO~C?^Go(7y;>h+ zm#SuycSt!_e%pHkw|7OAHi@qy9erAP<*#zi5|9e+zSch}@x)^P3oIA9>eczG@`H6Z z%@H%s{}fXtcVS)K#dG%k3I)oNI07atI23)V+K%@Nqt4Y^KE&ZG-HmdEdHE zBsQH(JGk|gkX+&78Q(Pn5?&pUE7Y$p%$}U`JEi)8vA)dtg8T2)Rey5XR)*fWU&>b`vt`$O zTD0-kmc>iO^Jl2aFHn3XXq(KuDnPO7*1vDNo2(U^xKDR3U%Y#-nwh`Uuk9;>_g(+< z`RCsmVmG|!Pfq3Bm$WD@>cY{pPZ~O&-AxOs)9mz2Y?ie@IPJA~$(1T*zV$vPUo{=4 z{t0?5_dDv^eVtiRU-su`pMHAlhEQzQ_Sbv(H!*4_Dp==lein7>-NID8bF+V0JYV~2 z{TAtKXCuCr^*?Q;z(cE} zrqx?Jb2fgwVVA*mW_nauz@gY$>il-bjjvedZUuFlZbNBr)re&MC{OQ>tXj>8u{c9 zq1~G;H!PWtq`hG{V?5P|)y4cs)31u=E8-s7O~(H%nh)-M8$A2jz3LKG(OOPdFYZeh z1-MJ*u8|6Fm&oaE zzLO|B`0}&7@=;X5_MWGw-^^C`w>tJka)qC4+=Ld6$TtmUVm}$CF=Vy!JXc7) z9p|FuS$UXE*Dl73e>z(tv+1I#w>0imO;lnMIbyY8d%nPO9n*_a$Cj@R*b(QH6I^yt z@YGSChr3><|G7VN%X;lg0=hqx?SF>E+2`Cn(CMx5sZT>GJ5VQoTDOdHc*o|0ckcJp z-rKcpR~|!`1i!->Pm4_}BLB0r#<)2p`SfM6z2VsCQ8=|tBj3+Qbf(i0#vG;vFOuw{ zxSex7jznp!h+S%{U2|PQ#aAxoW$xSFN*0F1+Kl6__x&%kpYE-?^4#jwVfSbEl|E0= zn|0c*HR}C>V=H(c{&!@#G*QEOC#&r1(}AqI))$J4yfclHS@o_YE)f0Fsi3md+U2y& zy>{lFIHeD2Y$9q2Y`#(!&rFw;uX5kGt)lMDr&nRKBHh&Y9L`uaKkbmKaB)!Ewu(2r zF9Y7LxB9`j^xFJWXYN%t2P^ic|KKMF%lF-> zcQQnR)?QjzY;D^0D>rnWL8AYA(eJ{+)3f|P#&6nLTD01J+a_BB6|J?dq7jD<-Tt(7 zU#rDDg;g_qyyt(MoHa${UW<*^M941sBjqy>~edJh!vU%i8Wx<2ff={b5UfKJ=y)NA# zG`fb%R(ap9i|2d~zTddFwEm|ETk90YWfxscXZ>&vtna*JTPV!^FEp{@+Iqv7Pfx#B zUt5+w?X3Ah!^?{yiBHMb+N5%R1b0!5WzYZA-!(JxFBSdY$18Uwe$LnCGwz;=KN_W_ zRvZjlpv<=YiFic6_5E8tW|x!G&oG8Ebgcj5@o~x7`B8jZ`y{R%e_8Zy`8yM*h|t5r zN}vBu7MmoQe9=k@Ry6beW@N5N*}sVKead&^mV(y{61JaQnWF7qy*eo7jKd4h&G}A7 zeV2D9bVzQ@JiJwDzkOX*@RFTVy6bPBPkd#c9uJS^*mpM3;U(A{@PH zH`jEBroT)NCNMCt*3{>1=CFD7!!pX^n)sV?-t)po=C=RnGHD4u&vx}t>V^gPS|sdZ zES~Rwk@#-4UtJFGRq;)(n&*4=%;<9cc)##nig*M2)Pir4f;uyTsvf-+ZjIJhwO~%f zg}9iFOhp!z9V?R_zp(vxhhs_?pP5$8R2|Ee-w%mDc`6d(f7t#&oND!HHiH^P?U3bb zZY|fk`)^fAZ>YcjmNPm_zbtMz_-^;|$3lxg2rqN_vM6q`VQOqx(DuH>=@Ko=;``Ri z$y{jeSgUuQOHk-pQ)D}1lj_a`Cp_%WESt4)$&NcoH?t3O-_Q2XtTa0|SiN>}#nJ)RsT5s#MZ2GqCSs z{6?maw{4^!NFKg*d=J;nPbu7|R;l0NI#_wgveLiNU*?e8!TJ}kH_B|?%g=GdtlV7c z=!Q+~Is0$tT=`}CXupy7>YK|-lJjIH$7TnwS$zJrf0EUyfba5wQ?q|A@}J*R@A!?qD>E;B z-);U|=gw*+D90Z7`1$eA%DDX(qmIvwab4>sX5G<}ZczKA%R9bT{Bh835oy7PoVAnk zJfs>keM_8iYkWC)eo(`^?y2!Q*hdl=16|pIcw@CdmCrE=oHrUB2?;k8=TQ zs#oyETi*_v;1Dl2NjB8*6!*fJc|n)VC);nU*>Zuc_kTWz;+uqsiEZ((Ds$d@KWD!E zFZ%q=+m2PNIT=gWl%=J_Tw#z1N}qW!`PrQq@jY56CcX@m&Ru$3(fZA?ANmXddDl|6 z{rV+f^VQtodt1tqzVmNB9NUxbV!ZDsfBpU5#*M39%TLu_e`smFRK|`~j~TSZ*P93S zUiO{oxZ$D9oDDN-ZblaDzq3_-&uNP(u^+`=Z}#+lJZ*7qfSA?xZ8|AGOC-P4JP>2Q zQnm6?{(%kpUpu(}T<5HqFhjgqh-WoV)1ITb&pi&FZF5}wR_ckFNM81&66ULlkzxG> zFP`h(XcG?z;m*INEjzO*_v>ES_f`u-O1=e7T)pVOXhF7IokpC?yhUHGUTC}SRP^t6 zTh_|;i=TgDa83;qvyg~epeo{ev~f1is)+v$ky>si&Z(}uC45!oYMH0M+pqpe{TIvl zKW{c@@4vSG(v0x(IW|{yzZ#WZd#xeBx%IB*zqL7k_WpKX%p3X2NSLR^M!j(R{v)1~$HniV862>7X zbMTJq2WQp~Whv`xHeYc3>}c`-;^XsepE$RCF`ryswlK-9=*@F!wxIl?gz4LIj}-G7 zWX#-V9@ZGn`=Z=H*uBm4^IUa}e|Oxj-+H8V%9w4+o-X}Rqs%G={%b3>+|Tvh_#ou= zEF`Y-uIjySUo8_@qd33jRbKXaz>@cE_K~yiPj>&_AE7waHhqnp<%#(n>ko*@TipBM zbK{W5KbHN$-wYpG3clZ1A^X?(t8z{D%heUG6B3SWz2IjcU>_n9l5_Bm?&oTum@1|G zFp+r|ZYc^dAF-eeE%{Ti7otmEIUHh1@ImA z4LRjjae#TByT{L-bKeAe3s0WCd30-WXhYhXaNe!kPHl)W zuZeuPGWpG}S1)wJTUid?d|q<+gu>g2evA5Yj`#L)E?UPPILD}Fp3$?)Gw<30mvagI z=$DedbWx=A{gzX6w!75sE<0?@^`G_bZSH$-YUZ#Np|9T-25sW{?ft)HTk$zNon?pCJxJutle;C+(HmX8;042B7qNmrr{bo)eyH=~ zQRH=zx31CNzpHoMHh-lgJnhb==77sLy``TlxugAh)9R` zwx7#czp5gA{TANvOD0>gK2G2WWm)RPz2=V0|HOvt*(dZP9h6F@bpP|MvoBn^)qAaQ z)VAx!+s#(~+w7cmB7RG_R=Sef-fs>}+y+|b6Wr|I&Iq19Yf;9N)c(xVTSXf*)7svj zIlA%-kAzcQW|;B8scp?2k9o}s;&jh!WG>$Jth@V~*g?(2bH-;cxEq>;%8|X$tfPUc=lx5Mg=DQ>rc+TVRQ2L z?_7N9ny-Yn55vxw&5j|nufFKKAiQYG^LI~o_g-3N(tJ;kHFQ_2L6=?m{KWq&tjmTE*kJm-73ko|-%%`+`?3OUudn?BAjazj&G0 zv;Rk}I%uH!|3iD<&*ED*?oDqn4@+C7%fb`RrX63y*IA{%J|St-yq>m?Ha?dRyh`ED zY<f)DzRuk>?+5eNz%6knj=6nd<1-L!DbAX;cFu*b=U;oA;=NWnV`07Q z@%=0p&60wTR4O^2+x%Oj_DjEud0S5l+niUN+3wnBTcgqobB|p()4;JPb9rN}vHR|S z*IxJgA3M3VC%DtuuC%?KVOG-7t(7GUw({kldpGFI>8n*= z@G=rnnO|Sg+bZ*_>&Dgh0cJBNY1mEs^XZlIR^C_fS-f+Z=1<%$o_9iRR;jv&+K2Sz z_l+A@R9{-J*cEGawBx}yRUy$t`ApL-e}k%5eYU%JE5h$$(A-AF#55yD-}dgu7dfTP z4<88oRni=A@z@Ppt#S?fu9TK4BZ;_LO_xa*9-IGKynPpc(lQCnwe#CqyKbrFJl!F} zZhK6qV0YxqU6(j!_&RMfTYYcYkswo6*FE;d?2De>NEP{z6#VEw*{^Ne6g2*yjjP+r zQM*>jmsxyKN9X;rrN(-bIkx&-{c}k$go*oM;>E9<*en|Sc^0p1h*g-WQ-1dIOmn9% zVR5=U_p^S?x;1~Z+|vGM>%A8@SeKSqNUnGqV$5t4eCX`?pMU;GrhQlVRQ{Q9<5_jB zc%#WYy!LuGU8)~CElus=o8nuyjh#bc`3k1)mBEhxUdW!^qTTy7Q}f>|wepQMhho37 zt$30fFZ6op8^!QNZeNr>i9Mg*@1M)d?#;0>V`;^`ZBr($blvxaN4My$_Dj8@RHxOJ zYxfka>&|*pAD?w%Tb#;AZEU-yp(+b7KWx$)?m_)m`o(>kKkC)7W2 zs7g6~Pqfx`OYa2ZgUhm#%BDVvcerr-GQ)|+-vLJi5_wk7RftcRT(?=2W$Ky!Jc;%! z|IEuPn5rWiCy5mJ`VFTuGzZ#Y^qwx+AY`F_WxWSe3>b^ zr845^qg#gN2b<%ce4W+k_*KdxbJAm0vFZs*_ZL6EBP(|JmCjX{lWWcxypZ4IaBbh5 zlmDcYmNfSGB?LrmJHe>mU-NQ**~>KtFV$Sy8K@hg;9V!JKL6!~i~H=VRFlPNoYJ4NYjG@jhKu`V$C|Nr+%O)WtNA+FIS zTdJy37YDyne34SU@#p!;(ZT&%C*{S9wg?nNNgV!?rMx31n{8V*NAdT+{~OP1>%}&vSIj zN=Mk%|9|%^TcOfC-Fid7nrFYZ^vG&R?>rK*Lnr6bU5iy!xwrNo{+HgKXd9-jr?@6n zZu2^eg?m_R%wjK>{@q=v%TyX{7N)-BzFsO9+~G|NlSW`ZA;Jdaeu8 zw{xq-bRGz83z0Ve#vCHJXs6yEcKz-sz4C%*R>GgxFG&1z%#StFq2gHFp{yGDLStEuktkJNhSQ&__ZvU(`u#D>dYRd0>;gsOT!;T3T|=v|7Z8RAGg07 z$KGF*boJuwODm23pTGEh=~0%zz!h7>I<78#_GC)al<-qKWf)XK6PGPbZQT3+;(>Ke zs)_7wCwCmZu*`RPh~J9a3plxA1bS}yZ;+YAaynqvcd1DYhraC-3^zS~{bcc!e}630 zST1#}+1sNNW)*hCF{@0$vVFGqou+Dmwf`fw-WNKwd-~@K?^L#DFSyfKyY<9}rU^^7 z^|j5P6}XFW=V!^QZ!6dqhwSoIV!Y1H@F!bM;&6P~RW9{ug`VvF!58@UObv-$ub_}8 zFRK|Fm9)b%vv>RZXVy!92VK=wG`(^&;y=gfbCJDlY_G1IFFd0wp%?q_2y4a#@n!w* z?T)OETQX;lV(q-8;Tm>=r>-a5o3D7S!zjr;yx@%|Zm`=sFbs}21| zM!WW&`cNcpxM>A*_I6_4^m@H!+1|e-;?X$iMfmIOu|)MnL3ec1*ml;+k&w zZ*5C_S@83vw;zcAXiW)aEanakh@8dzcUH|)(y4G&7=bwCW{S($+;r_W|8 ztvfQ)_}1|Q(w`Fdi+8ae{%G`N+r)i#`+oOsmzz*{b)D7x=l3=qFPBvQJT?9he?{{| zkHyF2!cB5Mt#OrS-QDO{wTjdI*hI4$`7`M+^bY-a+pu?uRFFo?#kOu8^+H~D^;Nl{ z8@eZ5i-~FY_A@nQ{gqJy)hjQ5c9$cILAXQ%Yt{`*F{$BzsG(8?yz5eOZyypJSw#?7&Q_f06 zyx)KH?#!MD7mIaQ+SGUbN#AvC*MqFY(5FnhZ$Eu~(6+`y=Z5FL`ys*oe>GlCs=boY z9H=^RiN?xDe;eD^?*5wTWcqZ2bI8+5(^ad!?u}mk?c1qE6YGQ;6qn1czg+t5(pkF) zXJS7W@{4%2-Zv3nVOYF9#&^<>im3O!E>~0co}Zw4?&HMk|GeWX7P9-B)lCavKmSPe zecqb4PxlJXeas=iRB^I`$?ENgjn7m((-Pi)c@VC0>%ES~|MVx!!PaewcMg8>dvyH2 zs$cofK(?aw$74$pf80_#olrH4YevTOYoA}8&7SLB@j1&?%)M-xSKSKl?4Ry4^h~F{ zEl>|etE2|JNxY~zc0=r%{ICd z(n|Th&r)YwweK@0+m3&Z&l@f$9my^zSZ4Z+Giy@#N1eS3Cj^vO+`jffagIq{tFlID zEXQoGAGR#Z%wH^C#c}89vW}mX64!%&rCn$Ek@fQbdw$;aiKZ8?JX&>m$JhU*7f&qI zHunDiV&bC-o3G}q`aa#o!Dv~VY_3OiU#tILQw@{N(;ireH;XJktD6`*gU>6#rS|93 z7ccnQ1Q{Dn75XM^RGXRE-_*Fj;>8UX3x-eM@A5iHbqf|rv}l?wS^B#&wyLFnu7qf5 z+>*q#4~(?e|9n&`@jAFWzgT2)WLi;-Pe)pK#j5RRi=Mx={J5CAK0Lrk(Qezns>o~2 zcYl@3^cXgVi3jY^5?Z@-@^ekCC+WXRCqDcyr)spzNM3t?^s1*_OOx-(J$Wy|!Etiw zho3WDxpgcW z?*wPU)Xw@>kF2IdC>AYBwLhx0waD*U++Vp0k-zJ{+qdrCe!gXgpi@XnewWZUk^Rh7 zn(Ldy<<}pWwr;=t^Fvb~+z*((=X;<_i->kt_4#wH6A$#?s*OqcR9ti6Zo%O{4sjD~ zi+(@bv!~JKd)4P@6CU@iO3mA~%j@|%4hapv<+lpi7jYb4Z(%m2Y325>$7Fu|jg$;Y zxOhTsLsrnPPqDstYa+^L$(;%iJ#(eX^|(#!oh4y9H}+gwvLW8;Wq;3`X9knPFP*-^ z#C=5l6i=;0K=$A030Ai@ee7NTYLn>7kL$mO&#&}fd7`A>Gxb&WYby&8zT2y&-Q{28 zIETOT|0m1YRdVr<)t_5undDh#ryOs6yYcun=8MaI&TgM__nlH|2RG$(Q-EY73$#KVnUym=J)+bPKKqIPkV{*!(HeY!$70;&ie$`7?PkgmP zO~uZC%PYoRDKb+utoNNUv#5Jl%TXa#?J0TSqt|VohjBXT**{DFm+hMJ_|dFQiPNgp zRNgdtsUBl?RnA@1IQiU}|83o&-vh62(SI;WdH>TIJ5MKxi&ouVKh(~e^)c4|YR&vb zj7O6V`rDG&lviuzEs#j-3|zqSW!>$++nIOo+|?Ao@Rhygu2TJ*H9liw5Y?k+dgZa=bEf9G=a!kWO-F9Xz{)}Q-lu$xI=>giImtKmxW_8Xc1<-b!t zR#x_E>R;uT#qrRYX53_G`}@4--SIX#(<^A zgkSfK-XrP7I*Dgr7{0r@={=LqM``tkatF2~t*9|Me6i$CNw^1>9OJ5#Bby&jeQ9I& z{MlrmT@Dw@bp#sRtrFk4{QP%zxx!<%D~1_Y7HpSdos|9yoO%rIH zQ8LS%8Cw>q9-9Bmb?cI~4-%((zIvKp^6d115}wz=Mqi};k3RYLd{NQ*K3B7ckKcC2kwpJ< ze@!FK@iXfP9{v7|!;F?k@56 z{Qr;R!-39AUR%8a-^zrWUzeCa{SHrC_PGiJt_i0-uV4Q!VIrBg`hVZ)=0n~q7i}8aIjp6unBFSa!|KSv9*%{QfFFSO1DvEtgA`t)liEy1nwv-`bA~ zO&2ZpEI!LvZFOgvlEo(D2uq9E30zKl#O{O6E;mM%s+lH>36blr)F$P zL3mw>P`T|>spW5%+)TaRHD{B<>S=A+o3701wlJt!a(ShRPLuW(B@^8ZE^#L=%nLVG z)D{rZ`|`nv-@-Tcn8L&C@|pe@KTohoaJ2ins&BLA|60!z5BN&@lBy2OzsI|B<44u? zn`PfPKR&)e%qY6}Xu(@W4Ye6h_g%eq%hqJi(%IXbBL#E)wxv#r3_f7GQ|Vs*<&YS~ z>hic%7apHq9v|3a@L)~Vm64U$2ZX)hR@qHcSW;@{7hJ}{?*hVEBi0WHN(e}js9D#imtcy*OuRJZruzwve@-_9_BOdI=qF|^7r^kPGNsEp(BD#ql6=gw8XyyFrt$ZCE& z$a})>xmpLK@mi-y%e?ttqE9(}lV0p9?P2sI-cRavby2-RE%T}6Kay@ulb!m)@o!|l zMhnxfM3Dk^5uHMcChq8A+*z@hTgwmE!=7K3q4%|Cg?XDf1C0u=qhpDocwOUq7%A(?A z`Ps$`t0ZI_SgQGgmAoHEpSM~4{?7DSYMdt;(-WK0)AAbp^_f2Mx{LqII@tgXGHJ_8 z$I@PSKYZ=Jz)C)Dp^f47H6Lg9S3BCwVhXki7iqpN74eQkxRd7;K0h^94>EK?c3z)bmw6~^piQ6fz6jg`_4UO zoONXOY^B=)v!)5?- zVfj%({ena0Ew{&sf8$p8zgCVA`V-+9t?O_jX|ai&YxDXgwgxM@R@(H&@V({RCt-M6 z{MGAI8;<<**{*kX6(jnw(XPrX-*QWIxe%hh#CS2#J|d1(+W(sbl1>vq$V>f#Dg z|E|uh^_%OmaJJW5f&89LLPu4uFX(P+vgfOVJ^wV3(ND0mn)`@&;Ij?)rE3oQ zIHx{OV(N_5+0O7Z&r(V1mT9_-;gRk2I;(bE{~cKVjCESSIM@$^2iUIPG@)L6)?D#Qc|kSR&8A$T>J)Hq&aieAU~p<+pOwzDw%_@P9 zT+yFxUQD$z`MvA({Y|IK?Cz9w@`Xk}YdL0C_i?$E(Iv_4iIYqJnZzlU=H{=Iy0=&A z$HEK&*OyaxGPdnP~O3)6qovWC}{;l(b+`bqO8*9b5i z)+pYo@NcVxNZ;9~%(ECP^Euz!PV<`dexq~p)`EQ&RypbXyNvGaHZ#cG9oeveH^-t- zr)~kW_Ce`Oi!XVlzxRyUQa{P2rm^X!aMsg3oqtZSEq?L3#pB*{A&LHO?*A*yJ)Dd%t()Ut{ks18b?=n|_qTJW+~DMF_p3en@L0Xc zhEMSqN`e+9&C`3F_~%TmSi9lx1&?BmIQ*9CsEMAF@LOo}PnXA!Qr7NE{wt@$ynbEN zqSge9+;wL)!&}`G6>q;fwg25-r}^j93bN<9NbKl8ZT;=-GA~~-e))eh<-aWRs{7b{ zO=yR@^{?1~@6YqPTbFN>T@{ijy{!4gkE`z`UrLsA{I}XGcu`KEBUC~8NR#VX5k^~c z;lIJ=N4JLc1o^jJ+jGXO-g3i32UpIh@LN(_g;J*8Y%UDGag<~ElPgc_4OookbllrK z>%+bH=x5^H&yO9*FB3{$<|Jn;HKVfb>;mOo|KnG%ES@_9R9eaw+P)4_nD=$r+ilA~ z$@khsO)~$NU87)s>ZxVHWf?|~aAjMjhjkZi%hPz)yh#l{E$+7aVW8)=3x}TREpXX( zA^G@JjdL~ZYnKSl5pKk2rY2Pf{ z(z^HC>)o}~Gz!am*75C+LCRe|SUX)Ti~jNgmVp8B4~nO5X_=_S<^gap%N7 zS$&TO;eittNk*>Jj;`N6g~7#ZBCoysr*_5*X`+np*GegTFBA&WvwE)aCfw!qOuPMo z+8TkIXP-9ZSzngBW|JyIe8op;=ff(GDh@ED#!t6>w|w&1q>JYdUz|2&M(eiYaUa#0 z3tr1AJpB9N>m75piO02^qw;3#_w3yLYv;b8q;Ba$83>$+eDZv#Qd!ro7Xb zb0K8EK&g-4MCtP$NkVyl1HZ3%S-DpKBJYQb36XIZF7WG3N;#VP{*n5PRhy!ME3Up# zjM;KH@9d0HgWR{aEss_jb}}wuX3CvW86WJ@)FuHNlQBlRmF4>+g$O z@6Rh2ZcpvJsPg}@%f2nLcX!BeU8y@A$uP&}K#;2W;&bQz{@GUHNb<=XCcv z?tiiN{kNovGc28Xt_5q{dFZ@nV_Cb$$K5%v)Q;Ddt=O^Be%^+Th<{<*6+0Jw-8p0F zhC1EbzG(r;U6Y($R$RQFeIV54z+|H%d{1AR-gVm%m+*@>^XaB9SI#j6=~ymvPwP)- z=c}nYTWaiSHuqDZb!U<{yQFb%yWSNAh9@4Zu6~I&d^U@oa->bVdtD{y=uvhh^N?Ei z)E5Ubzl6VCD|{g7qT6(SVZUvYPQ)INKbOD%Pwe6ho6O7Y8-8_^|G3l^&9#-csM0_+ z!uIZ-d6)e=j&LrWuGGr6bLQudFRfMB7iNI+C}Wl|Utwy_HRGP2p7S5CojEKZ!WXw#2GbpYhFe8 z%t&lHKmXFq3noAGm5;sHZyv(&_uwcaDRnPt!m#t6! z?%87Kz&TBB;atXxbDNq=#eON?ty?P*!kJpO*m7&VQ#AWtN3ng4&qC*ReHYcc?sY^a zBi*6sp3R)OYb)65pYJV>WvG=~o}hB@f}ek)@7bPbNRV4FM|^{9nG^-2UTzb*23* zq1xbg^?8>AN?)|hJ)ih>;-1V*7Q!+1)7wT?uscEj=KTGbMFvFiS+vZ3;VD+$j zkUMp8UAfxngWe)D_l4MHPh`$y*jn&VoMEoMSLHVg_F5sSlD)bP`z6n%Rp{Q{!f1X# z`0?Q>X-kheGijZ1xf!*IDf4mR(f{-8dtR`ec;?z7uQ97q?0LB7o5nKlSJjoe@^Sxl>fh1@k)epu{@HEzI@xxM4spPw`2V~rH?xQ(`aw+JR-`nyYH=5 zPhwTd1pRe4m*i!|^~ClCz0xoc5z0zgrRR3Di-G6ECoe%B5zhBHmfCM;vN^jldv;8B zy}hqfqgwv?-vi$d2vu(Q{WIafyzcFi?+lYl!nfWRC4&XY^A#YuYO8&bIow$h?!UI%R&wzY6cRJ!9P1zU=y}Tse&`t9yG` zO@3FUE1izqKXKKUFYiAU+Bti?6}){@V_I)V?0>H7d%nh=-hE6WOWUu@R#w|<%XR(N zb4(vAty+31m(!5-=!|D)Sh5|auGBl*YdQVcr@Om1o|txK-@@6iS{3~tzHJcrn){FI zmho+iZ@PAl8?p*D(?pHDE?TmxM_xHN>nG1LWqlvFGb*{Bk&`Q4e~Dgx%i>A2``*iW zH;X+pf4078m}t7wW%c6r^1nBPxAO)~_dQ#-{g!cvwfh_S#NuAj^&Mg?yZe0`o;gS9 zmmG8|beytW7XLSz z%bKxmn*N&C6Xc>w<1${a7yG)^(_rhmE`8m-+yY0B*mP|;Zklwp*pDfV_sU8UwRldq z%33Mu=`MFVv=*=SetIb(>p!>ay1<3|e>ux2L|%(vtW;H8n$mslo^)jP!^Xzu155j7 zrOnTg*RtiCc-O_FFaDq6`wOvbzME2CD?izC>Pm@ozfY5}Rn=$x>BV=rOhod$7rcwB z{(Ds+VORDGX2*FF;fItS9oV(|>stP-(C_w_mCu>y|Na;Bd|UFQ(o=l*Bb2=hKHC@{ z$yI15ZGOc*L-EupzRV73yEUh+W6WxnOUGo+_s%F?`2FJVyVdI%4`$`d@3krT?O$+o zTaD>@x!Uc1DY6roEgokn?hQ4wU!;{i)tGC6yO85O<$%Njd-wBOMdhtUEzMd_HT%5} z%Pi{&Zch|>ZKrWyPT<;A(S|ZFwyv1E%}Zp}=Rjp6pK{^%Dkx2e+i3^|<)HSTu=4=(l)YCEKR5msWSR%oN%G8a+Ms zfw^tx|A`uvZoWsov#*(6<7X!F~jo8`hS}(*!SNkTw-#_)0HxW6$iR)d7u5*}31FJefJmqorm27WN3P{D>3b zb6$MZbTTiSvhIJ_={oKCd7n)LgO)p`4`xe`6 z@#8DMeqY9&n-O8QhNm&WM}w7nevSMy&KsF8RUSNh!S`!H(C3qkpSI4uqkd)eC96Ag z*GQYpO#UOs^?##0}c7tNf^?yN;~fB4P2sgk_7ls;w*I5J%~!`S-Ks20&qh@{ zKS@ZQDAqiwQYOmxY?P$0gN>TsjEVM7Y9dswEl)7(d*{ji$mU|+XZL`|MMiD8ydv%^ zIy~2G?qW2M;J@W}=|z*Hzx30sTq`1H$-eew)pxYXWcfdpEqkWowr}??elBi)nWZ-6 zh}nxTE}N1?m-4?;>^ra`swH*K&1ADL8wIyKJDs)uefjQQkHWdGiB)<9bv#vD&)nea zm7Jj4_Ca>Xq30pC%R3&5cKkjWux_4KXMfX-Np9~yaqm;p?C1G1VfBR-vwvsv_4ovB z3aPrM^iobYuMU`hRT?vVtRT)9J8nubfg&eXM@r#4qaPheYq;g=Dc5tmnY z@F(31J+VSw+H*>uK53q^+o_V1=}n|pa96st+|q=*Di>a8oAf_sbkX=V_m9h)bgLWt zc`kEC_|CL%Gyij@uK0$|(FF~QMb(b_Tz{p`*;sC$YTvba!neieTQ4fg-aoaDE&5S9 zgIxC&j<<%Nn^J2}PLJ$7)Mzk2=7 zyESR|8`f!FItwD|3$6>_JoG<3WvAI|rG#4Tjy^%ot z+7!cGD`TxPPQK6ox?lL4f2yD0iuNZ#>&`P(bJljsvCRzJb(Q_{`%|`8&mZBcXR8{1C zxX06U?BnP5zD>)guV>kH@|K?D)ujyUIHWhpZ%Ihpzo7m#+qV@{XV=*L@B7soa`fuk z%l8^)eoWdwZ69Zwpzg9}{(Jk*+II+Q=_|HRpY@eUXh9|_(3YG-Blp%pI@ z2^)`M`0R^W-~s1xP*~qb=!`;cXobGVg4LkzguTI^X}IdJ+H3cqVUP&#KEu= zJzoQMidxV2nJf4tDe%O-{He@LkzJ4Yf*bQWa%L~uy6m<9_q5lFVlQ0RpIB{KE+Ibe zjP#FFJ>1)WXD?gvli;8S8n+j$D^AMOwgmt*z0l2ek_dLjC4kE6oT(8QwoubEt# zJVQHFy~O#Ay3%Xjh5cB&IZk$}(+{os&s>%%c8AXeo-%pwCnff`KZbXC;cAy1uD^s9 z?A~=DH;vuud+RFGD9P<#QyYw%|J#T>sk^e>yX?GA`Wxfzn^;@V#=QAk^ya408zrH> z$Bvphhn4ddZQpTW_PZLdl&oSMWV+&Bl^V@1K3izxe6Mi$9aDznwC3S1C(~m3}YV|N}8JzLPF=-8F? zo~i%u3*Wu&*^6e?>pkPo+$$8(Uf1sutNvln_jsMh1rvTKwP%Y@=Ly$xmTq7Cn%`P( zw!eubhw!{-UGIw={vZ7(8m3`o^y0ATj=3eON}174CHu4$MK?@Z-*3{n;l~#Bm~4g5 z7w29Gkv}GJZQ7^LiN22}1vR!Gkrun@7`N=~6+V+YJ0#)+|JXVPvt7tElP_HMvt8$G zwcY>9-BPo}Z>ajzonVaijXHRKLtk>j%=&vvf6raiwng$pZBXD<2JTi-%RjDH<5{aT ze#kwkXfOG+X6l}E$0`JGq(_R5RR zo2gXE6?1KJNIR$TIs<8kX%5=o80Xx*RrWhMEYNj3=Zn{C+fT9oRlamvdDEraieldr z4OV5g=d)P(2fy3I+*DLL%apz5J)>=_y)Ly}lzS<2lYkVW} zH9<3!jJIzva<=)}ceCG#(cXKo>+rYhfp$Bp7g=hp|NZj)$t6=yTt5((eAzl`Zqi-p zb0XpmdHc4o-uT~s@_`1o{3ThxP*oP!LrgAR8y}x$Pf`dLVO?`EGC!|&>jKst;b)dT z%Uic+?rq)D;46<`Ezb&@*nj8Yl2emIO!wZq<;c0>Vqs_OekH5*d_ND=mM{J`)#=#< zTiYiU+h2e8>ZyOK(x<;~V{PCs^Ft@iUabuIBOu_cVDF!?WE(NQ@>`G1v_0z+w8YWYAgr^A>AgfkUgoaDt$}H&=MR<^E0u^${pWsn z$@1?1t$Q{y95P%bQ=v5T_Qa#nU-XvN&5{4}?#wTjQj@it|Fla6#o8BsPB7OsHtl}W z5&3?1Rh>)A#J#d-zP%Q%f30e8MZ)H%Vuwplmy2H8|J6AWSL>v=eVngW?9s`U#%5zJ zAz8WlYbe8W8^(Riw>wy88gNIvD31Jc&LUXn(d&OvUey}Ed*Wm)e)2DQ>$sDr&p{%v zu6gD4kLHK0wi}vX_;@$fXZQPjyIq1Wt}x~GwN|yKrt&6k6g_Bd9yD*m#}5;CH(y@0 z?MLe@cNQ_l^s9R|%&vZDWx8g@DOLpyfoz*g-e;F{S?lyChNQJa{)iu!_`()lr z<;ARfy?xC~`$AXUO}A3+zDV*}Vd#;r)WCdT(~{Erhk2Skb_gw-^40hQ=ll~E&VEZw zjtFgQw-S@I{QRXS=b&v=U}JmGQsxIQqQsAEcJ@qi^m=uxtHo~N`6TJ=irZwSBN&aM}?Ahb`yRV$aP%DKO*DsM?RUwrZckLSI-Xgz0OL8Vpub(jG z+xfGuDzmZ#D!0G7wr94(!h4oUhnMcVZK&#Z^I_47t-HQwbk}CZ9o_Qz3Nz!vIjU!F z71fq)tk!b*@L@HLjT(EGR+q<`|CRQ0 zv%h`p%D>#|G@BeJ=ds%l>^sg^R65*u)Upl#_nO7-=auj^79Gyp+|KYfMhGh@m$P5w zbKLD!$#%Sq{ixHg=_>ZuA551mY4G5E!}V#+?1`oSx14Jin{0Myy6m~D|CVc*@2*vU zd)E2h!4pkyuT-UXTJO*M7t!l+Grn_*&}C)Ayy?-m7OT{6a&FmB{_~oX>7)I^c2{|r zL+#f{C>Cy0B-<;8aCTATm8 z>HRwx7#O&tr|rMo61Z_&Z_U2@chozUh24-k$0E7j{oS3rDYIC#WGdx?lD1 z)mqDmm!{V^6ovUu-IsBVW&4lKsdIPz=BWGqU>=k1pQ3Q?2n*gPcVD}%Yun}^cjBe` ztN)8Fk4U_z5slxumACJhNQnSL0_TdbOS6(3S5)t4Y2~>WIg7SPe9nu@T#)AxAYyBOC2Uo_chdJ) zX$I~w$v3il>rU>LTV&uAB+*i)bNA7Og2P?i`qz|pNMBBJ+aj^`WcS(XDVxvKTAIc< zsVtEdIG)K@vS!kRz^J3I>g5i{v(&C=@;ZLg;!t`0x{!*m*Drl#;Hz7FZqW&=k}tJl z*B5;#n!W65c;)Okt4Ck{I&0sMOS`&kPX5c_9^R&J9b$YxALo__mKQ}y{JP3}r@UrO zW}WHB4Nex8tI~bgAN*d`$nit;YKreG?^T6rF;nj)Oz&KE<(;Ik_niYP_!1g!POi*KgFGcCY05`Ll8_pSH{wh6SEx(C_t&SllHoCVVxNJy5$gqxIZ0tG$jZH*A0R?EB@8&{yxLB zb4l0#S4Ns~r|Xs%s%Pe^ny$Q=;TIB6G-LiEsjF|dPEUO&pe2y^;lQW2I`Q^DpX`eE zOOG;0t(94{@P_)4tmzk+t!9Q^O8&8)`^nKr?WgiTvb|1oyBk>S`KqGaPPsy(%MG8`# zvkq}wjY};3#abI66(wSOw_MFZcwOxLf){fCQdv(M^G*xddgtC8=d3jzx7Tz@zlo^z zES_by{p_*5eUHApn5eSk=gD_GOKvf5WYzn3jHA`oz-XV%Li_2nQZB@>UJ(1PvHo=a zcC#JVKMHo|q^*tqW12rp&st*5u0u^Rtv&YTTjpMgiCAHoE;Zw1|CS3|c3nxm!}ehA z{Vx;e*Y^H#7HV|8;C;8iQuq;Pm5|n&##}~TxnC#W=VV?`bKT?@6?l~Oym^hmyn2BT zkERsHr|pf;-rjxZ6ic_0`%cr0SDQ8Ga7w=_+$|$N!B5GCAwE#&eeL>+2kcCVS_*T1 zPj>9waOe_m#uT4zUvjrK^v{s4TsgZTQzFH6YV1VKyE@$8(|&AjmOB>p@@A5c+Q*rALr45BMXM8HU;Pp^nLiB-v#y?K8*zAH$lJ9Q+|7NnlWE^tHH9}b?NO6X8Gg^*FI=|OE6blpwF3P zwW~BSUh46`U)QUrzLQw{`+3HrA8e1V%x-(LUO>Io;p(h);lj549QkW3JC;YsTnqB@ zk&#(*Y}2fpHFGY`^NiV%TJm2;`m}3o$eEyL>PmZBP(U#`Ur%uXF8OVsQ12oAkAw=^J!jZ&yC*6P@(vj+l?$5=j?P7xkHu2Vd0c zxWq2Bx**(l>uCaOi%)yQjjLtrY<_ffz2Hxdu_`pqG!9C&He%a$dFqip+ZJrS@vU{= zLBX?Ly=zkPOlp1u7S&fL3o*YbPUZZTf2l=$bW`{be@o8lIM znldJ3DHE-8ixN8K7(WqR?3|sr>Vmn@XTSW5@f&&g?HodC{1wIYwfr1Xzs?t8$e%Cj zdW_fFEH2~iN9~idv~EaGetzoO?+~8igtB83|GzrHdstfcybR{I` zJA5g1jxn~%Ui5$I-al_|F7x=(b#GNCiyrT7#>ECJLfZsqY>P|_-p%sdW1^s}q7~bb z=r0~W=a?P#NdJ)hqH*clgE>3J9Fn$N{PglgczOS=huhjuZqf_*R3Px=6-PmY?ia^I z`;5!;ZhV^)7`wZV``X;3)T{4y%-P^x^iTZ+%Z>@WdEG@@*YA>ea(R7cm}mB?%UUbW z?pnEK@tlB;xeOW0xKBzPEfep*DF3)!Zml(k+i&rfZ^kvtW{SC3@OQNbRnFbI_ZZ_u zW3f!{m4}Xa$V~Y=$*HZvVP%hUp5N8&_OpIUSsd=0Y`~yvwg30#T&+FzVwPR0UDFZglg@hk_3ULGg(hC|N0j&YTwmuX+8R5_?V59mJN+P>%xIG(V8i>+a!!ni%$DsYW7KbYWT8qEdsCDc{(yyDIe#k?ti!c zhvLczhL8K}#Jie9C+7M`+j__^?6q0UrPTbaRY-`bb$jG#S!YY8?n|FmJ5E_IAlVmh zz5TLsspv8X_iewX+K8U`*I_dMnMQ8&gG$ED=icaa$oRbuSvg~M`NT5`CBObSZoPDF z#TJ)(g`H{LHl|lMZ#aK5W1DA1gYmT!?-hcqc3MrJQOqQ@c!kXsa{)#5nutr!D#RRv zIrQaaqt6xBzuo7ydt=t3y0=!Ry~4`=gxZ>TU;G~MX3oqTe31AYSZ<&H-p{?7tF@9##@{lXQ{tBcJSfTZ{E8ifV6W<*d6*dmY_j^?ew+2}78Yr1v3eUp?q^=!R&v5>9Lx7_=d3N7 z())f@&*NebhxutNS>YdZ1Wq(NFO{hMaxB2`{GFGEw?EW<6KHB=x|tm&_M$E|r}K2! z$JH};9_7|QylaW3%Pj%RH|sa&>MottD5Ehut6MkX<|7vM2aYve%l6tHO8+jX?vE{4#;r$>Xfnp^sqgvEA5nN}fxe^iiTwXHnU5BFDBE|;67lj3+jTv*adrHg z_t)h<%yT(dP}3}(zt8KZYs@cG_0qi$zPVh|G4Gz+TY6LFc7V>z=zF$hLWeC~elT@z ztKm?LmeIa+|3h9+)(ZPq5pzqTEH}pgJ@8@`+ZIOl6W7cWHx*uDQ@3*AKDWfI@KS6I z$I{GiadPjtLe}uT%(yHt`A10Mi5aul<_hozU$$}6n)w%#aFc(ZZns&fFlyR7($<(Or?(4KB)xufpE>0LX2 zM)C!z_Z%1e7ItJqz23LQ^*_SA{=aj$Ts`Zv_|dD?wWUlybU!%lSa6NS$>rU~Lw#ON z)yrP*3TiZF$?s}?rSh32r?R+C_E2z^-Q%Ule|dg+IHyK$_RE}!t5&@{($c?YSszcq z$ysv@yzZt>T_0@4AXPNS;@M`+-<@;Ro|!yT*mJnz#+y0UbBte?UH1%QGwJnMYE*oL z@$$z*S8C7Ov%QF5_xN-2#a}HhwxrB(*ACYBX*xcA@@-QuRvnj#&Pcs(w_a(odR^(x zhr1o_xj7UHn(TVmdq-*dGJZQDD;<-r%+lZTNozc#yC!Vd8njDqmF}({iM67;(>Zn}jD2p}is^HBWRxy#KOgVDa>;W2w0E&*dZqHM?p&IF@{5`H zp4)%ojkjwrwA-XnHz6k0*wQR-d!q3C2RlWdPTG7TPgL>vVK%dvU&kK*(?8-TIJ4r? zu^0a}{dql>%+?LG+ADXjIl8bbB6Hqs@5-7deGk9I+`TYC;o!eHrlv1UKb*I-7kc5ZWQKyX21JUsphw$SBmCo9nVdZll5ObT&pVnrqZS)cPXq^ ze{srWPV1gk{Ihjcw?4dRmAEm%`1oX{yGAuCE0+DZE*3HUx~$5C%88yEF10_5S9K|Q zW!H6u#mcHDBPRIUJ+q!2mR3jqxhDrZ3hTv}-(2zb>jj%TE_Ml=ES$CFJ8ow>_G%Vh zcl&&U;o2lmi92yuoEB}^yW{BBHwANw^<6%jc&~q)!2R#H@v#YeEK))~&)cf}NlkFG zmTceKgC7&-?3`}>mDAG8_L1PjXWMUk$yY_2EdOTfELyP8`0nZXcT<|)Z=d!|SbpiV zZ`V(|%%&5<%fmmNeG-3rWn7jP`?~W5tzy})p1<95F!jFE>wS@nGIwyE zTCr?}gW$tC*-sg!FyH-GnEL6}p|giKnY>Bn;1*}z(jW?<{ zOnj3qv}fNMBQ>a+EHC4+4j56?JcVjH)zxmb_4Q1+#vUy|heveNUu@12kR&Dh4Q z8_}<^{Fe0{jk8QlCuVpt&GA{ec&>vh+loiZC&C;z1j#14y<3wjviWR8pMhcGC8@(x zU+h=A=NS8yZ>#06D`k%_x`$M4|5?1N^9Q$R-?{R4Y9=lV?qAWem=U}2L+tlMd9UIw zR(~jXy`(~MbE{{pfH%Wl4b^s!^J{n1-YfXd{n3WQD>ElZ`P$^qeZ{pXSMGYN zkbH#oXoIDbwu=1j&t6qdI(<)q&2viDK7Clc-|%vJgkI&|s@6>%R~|o^c~<$R)4GC= zQ}wd)EWY*||0$aHPSkzL<$m^{Q~#%6si>XhGtLMcnV_NQZV|A2-Y>tmoaILa#D99Z zxn4i?nJ-3s#mqOB{>vJ)!oPlff7i}(!;PhJ%K2BeT{gOw+2Q+Zqs-%D9$Qal@(BKK zinz`s)wSx{vdLGPuAjJYy7|7%8sX-nU;gJf^|G30mT}~p#g{*Me|(qnoKnS2jcsN# zV;BD_Qtddkp{(miI_m`Wh8ZCq&;GdZEi;YPUGFk;>NY9EwR2t<-CSv1WyZCo_~w($ zr-@7Fe|}g|({XLy<}T$UOY}QdF>ak$xoYiOi+M-HWPR@@nNL1>Kbqy)_asZX^2ztt z%s72>3Uji5(kgL_UCNs{TPm2f40XF&qm>WLm>ILd-I^!Ndy4GN4v|gMGOD9<1a8N_ zoaEE_af7MaQ!7oM4a;I9S8sp6t@+O>2^X&BI}^6@>2^EM_~XkKe<-Jg=Y<0U)1~h2 zSv9qu31aN33!_~3b<2luNjn!5?KYS7@FEu-HWq)z z+6mQW-+x4?IR0DPX@A5q(SQFPwc@C`J00ild^gQ{lkm;y#qY8=-#GpKfA5iPrpg!d zZkkv>(wn8N@}DolYobGu^ZI`ZuS|pO^=9mQwB-NUmcQjaRcYE27|(V)A7m{!^`u2* zov}}2Xu*;W{mTMvXFi{*Vii6twpI7`sT;d^=lP3lIo55Y<>acKb87X{38jh+4be+w zWl9!1@}6#yycX7L>!s?KcIjZ7?RS=xO-YA8{k?WKv(Q~}cA?|*?*`jG@m{t+aLQ$c z%Pc$H%<4SJ$i3;yr0g%A-Jf5&ZE^qkZC}@ON-%ZF?kl(@bNKWs|5}HHm*rU!2mb^g z7wTSoriO!k?sJP@ESs$l&s$$!@alg58>KGp(mC2&9TppLI-Ovc;BZ}bnMcPRJzv>L z6Sp)nB_6kR|53J_;hWrt)tiIA@8dBSyKa7V+XT;@>gSGi-<`5QwOP-o-LflwQf>Tg zg|nYS<8Ci1$zZQiek^RD5Ux4ngW?>H3Ht5)aj`4o(~PHK;RW+f z`KWWL#+fDe&9*c8GjrKT`84|-Q;u|1eyj9|fBodc-TPMpuiDGbZ!*So&wo{zaVYtEJhCxTx;`pY1Dg)7pr<@H@Ym!3DT_T@c3@a)vV+2`~7 zgqNgBDOPal%%yAJ%gYlCN{*fDnrtQK&eOql@S!&AFX_XXAG05Q4u5d7>p<I$aXxZzZ<}cD( z%Vt@5&OZ6F-|I|e$V`TdRko7;9}BF`u`G4}{pZ9ww<&Mm-Ab{DmS1eW`j30o=NkUN zN##X?DO`)+d384^{<`P>@>zo5UX{Q}L7yi1b-t7-{B`8RP2R1KbxNKbxuErxOU+9C zTB`xm>A1%SMGtDl?Z3I>2k-KK`Km6f($_eA)BL?=WT3Id zQRIESR?Y20pE?bl)$eUxPn-w1}vLp^hnU$~FY&uJxW!8m#{{OjEyjUJBOWQhY-Lt^U|JF>= z*RN^Y>)ZaotGoTdhuK@_&3m=Wuk%gcPmZ+14;s4UBiyB%AI{!6OD$*DNliKB{fW$K zEzy5|?Gi}6JFRi=w2i%vL81OfOz&I|_nsuXTI!~hVijB2mWsH<;;R#{CN4S1?<=u# zYxwmC-}}zoTzh88Ixm}<*RFJE%z3@1ZLjpFvKa>>gG|cmcNRxH@)S5)>7QtI@xUQw zCwb;O=M1$fi=Wua31%{%H{79YzOm`6W=M%k+S?ucGkDU^g$T`%JE(q0A~4{7d)3N6 z9{WzMlAP69`$OPv^~DO&qbgRjf1J}uUwu|$XYjQ5Y`ZNZns&|W*>&MsWAq-UpI5$D z-9Bu_QoZDzMY#OE0Mn4`HjL1%TWWV2J^R_De9-6Pdygd&Z-!_IH>-n7z|w zK7%vGWq;W7o!7qoFZg`&{dCuVdusi#`TYMz7(&Sxb(K* zKK3HBEnSA&zV<6E+axBLPhf(Fb8Miq zNROWi{_%9;W;H!o_VUHr0X?djv;LU-dUd_FIxy>`{Or=l&YuF7eVnauf%E_Bm+Cef z)=XXPB6{OlFuU`C67A#6AsQ_vjSn_QzCD_(!#*u{l}Jq7%%oKrZQ*a8c!`|yi9fIO z$!_D8L`a`i;Q{I97D9-d7y#~IsynLcJKKc_dPs)Xh4?D?Pd`7{Je<-V(` z1`7T@var4@r_5b?YUBGtPcP2}m7nF93u@lA2FL~O_dE7o$aQ7R@A@MGt0&ZV_%L2f z?%-KbR8+Zj$@kl)+fJR!c%xkYqWs;hwN>dyue{H6!S>LetBh^LO4^sQUUs!j;hA8>URt zW-3U2w#yU!`|{R?gD=>QJnjp)xMPF;TAqb!S0wN4%TJlIB(WiQMUArb&G2&t7gwmS z`q=z%Mf*O7`iat<$FDj34bFbYth92r#ObX%Sqm1=^mwX}_rLY{@0H6I&-wW~H^TJ$ z+TFXiN*}i}sjU8L`ld1I?BgFx!b|KV(v2>@Y;${&^XdHp)hh?Lg&XXh@c+qyJFm;) z46jWO2rR16=7$G_S6wM%!4 z2*1~o=KcWwbdjSq3+;*v?{$SlRpiXzTDrPZ;a9Ja=M*old1jxM>L2=je_@AS_{(_* zZwEg;A8Bj7zbKI)BhDu zPPvom|Cj0GhTbF0kELZIzg^n6z3I}U^SjQs3E0;b%O9U_KH(OVm|CaeK5qHe-<+1o9CS9@xjV`pC;vyoHi+y}nfr>xw% z@nZfD-YXe@cdTwPD8Fc^BXUj9AmoGLf!3(^;gxIly$k2rlv2{#erwZ4Zn#yt>A->j--EL%=W#>M^}~`8Yj>0@)8hvVB{EUUh5pSHszk&rdcxo zjKsW-h^qYfwEe+rM)~!}pBlV=KFgrwXXFhf5icDNw{>U z7cj(MX*jhw@{F9Bf`OLr|8SRrjFkp&>#I*5d!u9+T`c%9`F5ZdtFA7aWV@A5``d!R z71i}}!sly~Ri{2N7p}d!@pHG$+d1FX@lL9=ywaaL$G_~>(e;8`*~5ewU8SDv`+ik9 ze*Q20l+3sf-xA#YeV_CvaOIy5Su-oQ@y*Pa0sChclw}`LiH}))!sME!&SOLF$7*T! z`dE||AANLhcf1f-l3b-2>O7S*e~M=>-#gPu@t-eDue#kR`RLCZThqlpN-S3Ej=$(! zt(3X>L1^?RX}53sr#NfwR(MCJ%zv`|te!;5q2{$-ZsLh^GsWbu>Wa=hEtY$sRQti@ z>lS(52@`pm5=8D^Q>kBNR%>`MrAfJ~=KQ+(Av_h*?CO&(9~Xvg(`(Mi68V?aAE6nx z=2|T$2dBpL%W86!aY571y3Xkr%3o!qy(8_&10}=r+uv88I@VEB945Wl^5OH)Fil40 ziY3{rQzj}Zss0dj@V9Gu@~!O3kt=U~Y)@25Zk&1a!5equ1mhR;Q*9?jDR12KcE+?a z<-(h80hJk`C4meKYF=Beq=Tw7}39An(uf8+;vB+7WR^H`anYBe+=bBHHM!Zfm zTD*c^d#O>$;m+B|EakJpKJmL4ANiHI{Y_l#GN00f33FZZnwBabv)2w_SD3oW|J#SJ zUmY|yOkFAb)p64OtiNf2S0;3Dy;lA;!8a?U=j^RD?@DBk<@`G8{_&1&b%Vw-!H^wO zLjOn{S>Ms}>7{63nAx5bn?&_a)4m-{Hs>xlM=tu7KHu+o!LmI9hp)=tufHYz;qJf9 z(@wqAzgX_J@Rin;#tZJTN0#4_m~nX7?d%=r;@+s0+}!diC$0N>#A0`?^QICBI-RyB z-Z4~!uc%&G;;k$DU;fgHN56cnLSJoY{N{9Z$77MlTfhBT^ykMW4#zt8weNnf**fvh z!lrks2C5-mG514u7|Crg+vF;Kq)@$7$lwp>uSJHtR&BN1rMa)+$c5kU74NNo(~j+vR)}go75ig9#po5=+X}hAOZAtZv#qr%-mRuO zPdTEp@L1H$6Kc*&Hs23SIAOcia9Kz5^HA;g7471m?OE4Uf6}<~Z@EPU({ayotM9of zI}Wr3#56f>&1PLTuc$8eo#gTZZ{38HmL>>J3w>Jcp8GBQ#_jqq%gugIU80yS8@A$0 z>gL%GriW>J9W;=fAzC#r;=l*fbw8%8(kSc{N?}tfx?0&#|9Qzwbyky-7yY`q$4`m( zZ5Cv9I481qMp=l<{LYtqGZ_^18LjRt<2y1XxjrOc-=j_L+~TB?c*Re}u)R&mn5{LbkbTaU`U>$vtsR7c#{tEa~; z^)Bo1D-EYJ2W{OQO6+`3w(mRpNJjXuR$1FF-9zqEWs8_!HL$L>ysVly-6v44WATpf zMLC@<2Uc!td6CL}SN!yp(Cul4tC%gj;!Af+WF`p+EnNAa&hpp8cgy(S`poX?Zu!Zz zciQU?4}rCRB9`V^Ub@w*^;5pO^d7-Y)nho@XL!(8ecI%mtYA zwocq-m_1wXul9RJr`cJhF>%}#nooY|w&w0!e(tqSuHiXXU7l4uk5XidEsn4BDhO3I z%ja0@DgChj`_aQ1Zze^iSy{PlyD2mKcI$#n-x>DFxlv`jlIDw6rG%MU$^Es6*ne(= ziF@Fxrc5ucUvV!ddw*ptVs$rZdUxbihLf6wT6z86sMA-B0}6yvycW$6KcwP3f2qe- ztA@#`ae~*2)x}pViVs?Itzw$fqV^QQ>jhgQA9p3Xw-kL^Ewy&&dv*6bgWVp8GHJJ|RSiOTM#)j;m&elZCF=F&yOnIq8X@F?-W< z>l6*K>=WezQGBchf-U-OjH~~qJDDtWFo=!Z|9#8#S#H~p?(hgMcTuZ5`P+7t{NY5w z3-?^!MSGmC2<&2vJ-`?$6(+KMO1{&U@BcrGC}+yqzf%opn`8gz3Qx&KN40C&QEBrZ zv^^F!y|_~TSjzlh&d;{iX%WwyB-e-DslJljVDcjM;&IXS+>9HAmv5*9ybuvA_}%k)uls>#=NaEC{aaW5=d6)uUu@OojJ93k zCYirkY#&Zc)>QUg(7DHLa&yg{x*LCLuf-&rgj~B9qQTZ{vW!1(OQM=S@9dsS^VDYF zpMHyX>&`h7JlDMEzPv}{oBjWsDFuc+^X(@!ab0=9WMeTsaNgbZvpMwNZu;15b4X*& z)SM#2xpp6AwrDQppV|1=;e;nguYP4#_~HDjPrF;hrbOpZ>VNb{4Rh z^**WGrs3ee6KfwOnm=DS|HK9dn?t`ej^B#2JabWW^MPNI^VRO0HBXz-Y38Q4$V^?;)^DkO5WD`oi5$~WiOZVv=y3by|did|rn=iRME9P(O-n;JKg4`qDcN=ACW-V@CcO_bfH!jlo662A7AL9S# z^(T5CxmlF(*LCGxHqrkzz8_AdwUu4ir94}X#mY5SBSi0oMpWf{QK{GN5BO>m`@8Gx zbGXZ_?rV5XN>64kQQndCDD;dv+g4--}6_2wVebrp9I-9K%ciZ36h{+ zvGJL)*KbtVSMz++vIWm>EVZ35TOh;IA^Orc2m7`Y<(5&)?l2uTycm6L?y;hu@c|PK z+}j*Ab*cX8D|>gV`UKtbwu`Kr>6W%_+f6NvPdg%wWmaT(^K-4SXnJDu)@{n=uZ~y3 z=3kxGywxV5;@_5ML5&yQKCzVCS{ca0S*TUc^72g3=~p?Dyh5vDI|O~c@)hl}NIH=2 z6gesT!DI!UCuxDFdS#x4?l{(9BzwU?d`oIZd~IF%f?JyVE{C33>TGc0OR~hN3eVRO zX_5|y7CEnt;9oB<@oiz`lP7FvWe%;=D1A|x)wp+!*Y-=y@4wCZvNq=Cx5X1?rkG!U z+I4ki{EK|2xP%cPUf8speg`?vO|K#oZd~^Rwc!O7^_2 z5m|HDOUo%GJ97Co`*h6@d44t>ktg^v-sc5PwB&u8ATob?miJ2DPm2n6%m4B}qu#kQ z@`6=O=bo+4-!q#&TBsveFTHT>!&9EUCSqk%j)Xol$Y$U=`DFWh7lwNC;%aL*c>2C|a+UDPb#7-_MI0%P_)@wAUNw} zZ_LV`#;-qQHq=|@_1@pS|Nr@oH^rS6)*jQ=PkrU`-S+T@V;2{GKEd|v+SiM(98TMAa`ZKMw&CtO zvA!yHd&Zfb2TC?Fb%p(L^9?)cc*8d9=;ixyoULDXGpsn3cmMN2vy7%4O-rV5^?jH( zE8&2D&dDYJGXDMy%P8C(Ch^JR#?!y53)hK?gsRC@DB05NwYW;4zQqduLcP#QDhvO!2|j%%6rFMT(Sg^kT;Yud)8+F!E?R2ool@Uz zy8G<;W9=rFm|3*0=?8z5eJ*A8N&ma6Ud{HcO1gJ1I^T0M73D72e(#r3^@`8V+nZm% z;ttSzzbU^->9(`VpZhisc$S8VCWZMG%s1ZsKW`Q5RGFKW>{?s*D?He=HSXw*BZt+5 z*KKs0woamb?+GFPz1JS}U%OVDbhO6tcS6t6gEFEDK{dYH*gwsw3E!h}Y3Z@rm9uy2 zWR|~waGqUZ+bLd;zN0hCn2PU*mD(F;>G`j_{UX*-yYT=cV?(IlRrTBbeq3!|Lm0VU zOe^gDb26zl^ji0}Io(Qy42uq){C6`@`daeNPrLH6^Ir>{UgE>VB^GPW`848XXSxmJ z^4IzfV#hCTIA!Q4D!6V|{=8VqO$V$!wSDfNAZPpetPUF8(Qhuku(Eaye0e7B) z@Z27A#$|WEPzqar!4 zt9{zXUgsUXf$#slzh*hPZNAFcwH#$v7M>20+s?e%OEyU|_nNBiB`sdo{=) zCHK@7=Par14^~-F$5=nra?V`t<+Vq@wc-IRSmnv*vS2F1B=(TNcK}W{^;DtfhGux2C@I-igIO zud_$*)t&rw``ZM6Nw<0O*G{~!cyaiHbv`Srs>;J}y2-EN_?JJn>s++d;_t*l`56AL z0}iL1Y#g1uw`pEuNejQ=@8aLFi+%D})oB~bWA43AxO!TFB6MKgqvy}Ec(ekVK3XSU zR@Ps?=}SbK)5O#pV%?D)jjvC8KUut|aLxnH?`xh(+&LhSeptX;*zT_4q*?uU%`-Ru z&fN8jdG#NezpWp?h_m02Z>kkN82ag`*<9-=Q)lHr?nj?0bzSy1dMJAEN}9Rnvb6%X zX6?cPtDL8au2}U?cjgXWZ`YdjAyc(-S0wbkVN$Vta?RGtZZY?hAA$dik8Jz%Lhq{V z)vfuO?6X*JNhZHs+~eQqEWTJ&>9fxJIsZzJA7nezDZX<)yVrwXn-<#NP0%tfw0qb2 z`_4%Y7LM(0g3VLb&wjM`)$?}&n^$g$Grz(8TlwzI`TRT=o`<(Dt7zEu)2A^yIy_+8 zGnvL@-3O*+adzhaq~|!#X_zw2Z*|S&f6VusxtbC_7na_7^Q9nZ0pr!u+nMF2C46(P zZf^W9wd-#7_WvQ>@=K)FHRM_;>E672?75t*ALC8#@A|JE-kLM1LcRBfB2TO1Yu)}! zLPa0?ZNt45H%Kjr7OYS(@Bj2v(rVHn*S__$H~G!@s$Et;@6vH?8=rX$LJm?}#JSiU zj;6`YmXa6HtF=0GvQ9cq?4Z4$-+#@2H}a!aFERaFn162XB6C~Go6(bJwz$l?%imMG zukP*jwIPvz5;@mpKc0Gc-7BpRA6QBa->l`iQ{J}mp4+C_w{w3jI54kQ;EMroshQ%@ zy^ngUYRy_yQnr4vZxdVZxV~YptCQjZ#b&wm#%X!C!{4{H&bd{l>79CF#^i0u&e@hl z)5Si2S@Unk5%%7oe`^nKU6@tJ&E-^*VrK6#gB7CsbZ?GNDX zV0~0|e4|}x$G{ht=bp1Y|2$oGR=N9Z!3GMOj{Ix8m=m<+~&2w?*cZydH ztZF{g?ma#6^Wn$|$`@EY+AbgJW6(Re=l(MDdBqK`<~P?IO}E_|vOMUlU2^Tdh%Zje zr*`u*G*(_)esQ*>zW2p~Q+zISH*YyybNQxiOWuLbpd+WJCTyv{w|PsT`Jx7TY+ zT3q0|WW|ik$0TxNZOH zYSy*vJ4XLMR=-*Ed1~h3!tMKOJ472dcs~ikhP-%CyoKb-X6{qB!HO@_whJ$$?OXP2@4+I*a~ z#dosrrd&1)yt9-q=$*hdmDdYn4kk&zojJwMbR};z zn|AjSw*R+$HOyQ$sLbKb3vA%pA^7UpV)J#5e-G}ddZ0DYZ@c8wuyYL^iLazIKM3{& zA9?t7mR~nVai_US!tbwIe>QlkFIe_g^WFc?D|~)&=e3%AQxNu)%eF|qr}j|VWaG1o z7v34%ay+*G%f^Q{iWiG~Sm{-!vR3PS;+lE-n}5koH`Dy^eAgCDUz_>MUd5Yaa=cNeBHf@TYD0-XXLt%6FC%*T?*5GDiyXGr0={__O)1VTXTlNB*p8`iP2jj`uV!a-Jh#>Xvt%6c5%@lRA+U~QLt zbkg1*PYPonsB*vDIbnr-rt@P9w&KYO-1)rziMI6*?QDbAeR!{w>|0i`;Ekiz+r1|1 z-Y)*9`(w&+(a5fS0mXl6KX2JGNibu|NrvsJA;-dl*5;<(O$z=wAoJz zOPUsMFw9$U@!%_uzy&L__g5OeYv57;^k8Mx;Yf##U9S9X-Jc6)lS@vS z9^2Qq@hVO$%3}OddHtXAwDy9_VK*+^`Fb_&NJ)X)^4#^JJIqgpeqrszo| zhyGs7l*n;+s^P_g-jMg^t(OzWs}M>?IaEb5gY37nUjKJe9UGhjxhwJmh)JyRasA#!=Zd?f;akQWwov zR48^|@aTzJ)y%$vxJND3-`!7$%~ja`y2LTqw}_!)O#%Crid~t)2GeH;9C*5m<=4kQ zH_hCRt^M{;=TTRXXtSrGS!jH4%pRq^HyPDrm8Nk2E^{pv@b$a?Jcf6I|FP?P*Zun4 zvaIyq?MEjUCNh4Ts(*Rjt62>@N;7{vzIs#aV*Dh2 zw<}UUB-~Hsqs)J!h^61x>g#B_YRyten{+s8#+Tzuy%+U4ZM+p*Atksb{_?9uuGeK3 z=g1YT>-^oxD!b=#AmcyIDesK8J^D3kxBsnETjeA>>z=D_3TkUIH#dCtz+aei{`Si? zo?+9wYYx7W$kg>a$z+nummgBn`S9l7+sA&rzd8H*ii2qp)eCAitbeh3mwSO@ZLWXB zywbee7Eg}7eflYn&w#ngiT%SNzVs<~^bfX7G;mnc{Yr@@Q&#+0@NRy@>zQJO+xpjsyWI~6S^rlkZpSkD zf6E>3mM)H)`F8F*Emxc7tH0Q+6nB}#=U)3&GXKL5juiqeVlOs0$Ch7dlwPC6nag6c zeCy-&#aeTWt1Y;vN!@$Q|GZWcv2mcxzY0%hblZg_l7w+_&lsuU3>Rsv&hk)6^E^3 zW}0kYFk}5z&U217e?MsbzWpi2Yo+`7B8RDFXVRub#yl@uzu~EraB8RC=O3#aO@8f8 zOBWAzH1j<=>811zE{&@TR?a>)f7okw-t6arC?^{eRX`w(9S{vzChP7q)r6(_MVi(Vypim+vfx`olN! z+phc$E%cX)UFLO&ZQ35EV@JO*DSfcqTDP}kV%(ceeu@>(oD>Z9e!8Of{@B|jp_@xW z8iSIj|6a3hHgDl&>z~4cJ5ocdXS^)T5h-siE1!5af9WnQt#fO|y*+%cgsH#W5qNWT z{Jz_T3|>9`lQVbfq};Vt+%0glQky+rbmp#Dt3|!Nv9I?{{n^NLP2DQuj;Zgj+}HCjapf{(bO}Kz4hara?KeS4rol+ zW3gV|^!~gX<<@T>G&m-RO#2mj#k5X&if?4*>fLS*?k%o&CZ1+~_3r`y*>9S6j2m!2DaefHVUcF0AuF1dHzCG~1mZ^Ti}k+ZD3!G#r(4gQWlT}}nx0fw zyjiIvJ5WQvV%n^^v90?QE4yY~d|K2hcx|uO@{`Zk3pDRHT2jJtdPD2Iiuk`ZSEFqL z?2L6H3uQN~a_;$ZEci+le<_GcpMsK!0{1R|) z)Aby_wGU)A&eZ&E_$PH$@UFLSAALMEuaLNM%drH8r~D-f z$16FxD}BPVncE$&XdLaduB@FTWBWRO`B|OdDd+XeS=TH!UKFeKdFkmBOpmPun@V;( z{K;U`ey9HH;)jo)PTK7=Z|AZDynH3sJogsNcwFA%TwklAKlqeitG{hS zfpUz)Y|(pZ$!Qnry63U@-R=r(R7(8QTkff?b;se=#aBu;hOLv2R(dadC%gYs!W`xo zE+>P3&pLn4E&7?(ud8>T&0jJ%Y)Z<p_B#nZ zoNMFOWv);l1pI`f>lhxZ$=R=*?mH`TQB_0`IESN(%}=2y4Nh&YVq7%(4{IY+bK|YUxnUU`YV56Fc`bZ7g-_iH6n%MxnQM3Kx4mDD&ogPgVY{&EaEHFacCX-_ zFW2wm|I?Vr{qL+j_lC*FOZGet_&R6#p1dW$JiLxwsL5}A>ZI|=$%Nlz?jI9{f{nW0 z=SR;`cbd!KBeQ?k-X+|?e$n$j!4hwHLQCt+g_mv` zyu$lF{K=jgvwC6IKIH|v?T$6sEnY5X3yO~KuhU}f_%of$L&>P{Yp1JT?u0kpJ<92$e&=o(*`^yR_UcpEr5~@KJB@LM!0p-_hPzkpPGAr% zXGw3`z%Qz2hr6`A9pLS zI;S376nW^O&XYq0ZiO=*q{gX5=6*9Xex~MhXr1P}MIRdCr_Wzww9iig@xti9V~b#%_cDXD!`E20+6=_HRUtS>XuvOw_o&M^x!VYX_TzVet7dR4WGjHOq_KA#3WFq)~ ztte}Jzsb(){Kf8bj{gF#9Z<+kVO@22>fOxQUfToSKd$;vE46dInA~)`#0E+3V}en= zJG2}84n3~2D;NC|7bJVwQ}Bqe?E2%}Yc?&}Zn^h%V9|q!66V&!eiaXu{s^2p6Q~um zwd=>JdP!Lk%>#EP_56~wF}*rZ*8dzg2op+8kQ2W9cJBInD@Er0NMhBT!WU33-Ssc;`KO-R(}@b{ z$1C=IO6Qo9lND%cm+q8mv+u!vPWQn0Jupa^-U8lt_g3e zkNut~IP0#`f|RAn#~!_##$q*J!1>a{ulEkF`QFTJB7UNXLwbSWOP#1=Ri3kgk6FAG z<80pYbm?U^&V)}_Z&&A-@R=l~mj&ote6?WD-ZkCn@@-qz%xV!k=`Q~IqGX-szL#hM1{&9LRVQRKQ=*@cRkMwd>fDa?~8w%~XY;-41L^Fwy4TS~mF$Um5GaqPF`VWlv;qPy~@Cks-9 zCd~RIyQE~r+++b`vngKveSaUT&n#akxb|e6TtLDR%@kujfw0qy&F`wrTy}V~nd6M5 zVV&o#p3FPMYao&Ny~oCRxl@P4+k(JGy`Y%%4Ln{T;~_XTdm$zENF52pT%NZq*%=XL5>_a5<5C3Ljf4JJveqFEX$MgcLq@A8wyDVjI^XSQ| zvn>l*y*-xg;92Q9!vl+B9IrGA-K*w~o^bkKMh&WGyg4D*)4{N3oTkaC(V1^F)3_` zNs@_C|5G-*CEF_$w#;~W{mq$Pp}+T+XUFmLXUskBR&C_Ji+jq~Y1;GtvM=wS&>+4~ zqsDqoOxB{~x32!5pQ3(n-IMw~u6827=C8N-pM9o2DC@V;!ISs&6f9o2^rZ&(E|*@k z@%yR|@8ACnp3Yo+YX05jmiae-tG)J>o%@(;$;m58MxUCiV!i)tSeE-^^>JY{r>4pu z%cixYtPM^LJ(M?5z4q|qDCMosUl$u_?`kOi{-aS}?En3(U6V{@qFFqXFJ=a9`F!4^ zO<%Kr&yi5c96ukn39?hw7kck$s{EgOYSk=x4W0J0k+DX?Y#ayjIk&F<)b^*H{f6M2 z;1{=U@=g{1p?zOT>OlH=@6urY%?8WAR4jHmHf0UV(UORBE3Q31>8aE5TzZ$i)FEb` z`!(K+J!I70IHUiMkVJZIM}uMEy# z!#DNj|Hk?ht92IBeCu6k%_slaIAI%u9?{oP4 ziuB{qtV*+GN%oG?eX9NoBP1o+Eb7;Lui`EuU{L*S?w?&Com{ zY3HR=^CncByU)Gxi`*Qcn%B1(o9-~@?_T_an!$RnM?dvB5Xz(jJ!5!nGU4 zXI{8jKl`l}Lq3bbidd#KH5$9yypMPE*-SCu6glJ^A|?=Ib5vT9I>g z=3U<4SKofj-k#ZNn^nljc%=)&@hT+cfwn)DFJ8ttV+qXeKN2_Mr z+*QK<{fZepclr*TE8D&>)8)q8pI&}x;XCH9UmT&3ea2gS_D`LB*|`s1%fEiHOtJrY z=j>8*X1z9b%c)jee?r)A-*|t?Fn5B|S9^uvP&d~QWv4B8z9roLpk~{ubfqU(wtl!8_5A3C#r;dT z#pTu+e&wqd;mg@;Cf($*?xU58SaZVh_0LT<_hid-o!zikLzruU;GI>c5`O#MGuxWA zJWQ(Ds?YrF#4a5k<0`EqP5-=d3zsy{a=&tMR!GT*%(XSo<)b?)7gZFl{#4~zSpKcn zgX{B~q_)YIz4N;F-{*eE>1z^Gw$xE?XQsP?+1X$J6^t71-%PJ~Rrh6Q?OA2Lk3~B^ zSQELy*wa`onne$8tsqHJ-l`nqS>7E!a*$wE&{gw7YYT@%?dX<@S0 zjo!5}|92m5Vwz^gzg530h9dna_1T2>ENjDIe=JnhpR$BG?Tm{?#4(x1&Ge6= zdc&+oU!R-`J8Zc+Z^!9^%6FDp)`v2Ly?5H4y`9>l_@sh4V9(kEPWjgyIi0>){{3@A z?)T*@Kef~rE)!EyjC@IMDIdNA)cvddkx3iyTvRZsFxA-XcjKg+wje^&~_&U=A zVQ(5VpUy0}=ymJ5+4{gAUjG*?lG%Fj&&MFS`I}h878c9jiKZ<3Unz zrMu<#|4ufwJ^@wF1xwj3wnWzoPCH*-7xXynmFbKtRcjUp*#5eH|E|5O^EXTJTLyQo z23_B7R6aG;Z>Gwf%|c2Zk3-5|{y1}d-z4VtLJsK@hyI@nbI<*tJ30Qo7wPh~Idb^firtI9WWV%jo7j`}_D;a675(fV4wkZU z6!x7+T#j3Tj!3`0^#4n~h$3_5`pJXy9ESGjClVQ^x9Z9|D_8m7J6tSKK=Ab90I4&Yyj|x^sk5&!4?j z&{4d!V3kbod)?`QY`n%t-L2l&B=pwAn%!vBh|C-ihxVEN!qKfS4oV6=uw{P05S?+jk%dF%Sv;JrH zuRhj$M4sStwRf4fck5$;gXQM!tI{LhNvYMoi(ZoR@%V-LrwXiEZYaFp;;M3O|C_Tn zw+hUPzu~`Oi^Zd4p2N0g3Cw|R+@F5cE(=fgxga-fUwd7v;t5&dBRNy^kH2H_$<*~r z_RG~1@{1^##?yX)bym$e-?$}g#XEa)iXZHsv58HUK`rGZd#A^zBaDY{dr2Bp%63-u zFYP$H|FY2IN%_ss8TP(fcXr~n_vf^p{EXzE^_Pp|v3Akptqq$})}=4|&nR} zrf1jVC$5%^b4(Yc)%+0t;NzxIsI^vxVV~<=Rr{*CRom_wNONu6wf4_J_S!`n3NtT$ z$vN_=W98E=Hir_{RC*_g&pKdSz3_5{#T?U_Hi5;(AN4H-Ij>w?eq8?1^~LY=?e%A8 z6zt0S{LOxilWBGE!tBpyRwX=i)Z;OK_&KVk+{>ps?dQvoc>p-Y(Z;*W(!_^UVogm@%@{yz3-*h)vS)}Ll?CnAGt&x!^>{TObg`L-S=tn z>~#*i@2%$OJ*4^WlciCI)XYNB(-*2Hey?o)d3P;~)vAAnOJ%HF*YdTuzWw<&F!`U` znwPewQ|_wozxw3q6%n?u(9}8m3ezU~zcWawKlY)zaMtv|MW6T%xwPw?yxI1=gn#q= zME5(AMKa5S7q1gPrxAU1^}YAi9sAuPieHxL&CJ#AHx3_Fl;*;cE^tU)9R!Ri_d2)+hkF(cyjSHM~@eBY`z&9%1Kl6jywp+ znsIQE{iLJjv)>z^f7WlNTeh9SrsB(+WBGqYB_Ag3cyDlRO;dGy%~SUYjoyd%_I-Nr zd9S`eb#K>J#pOp&RMg8eMey6Xyp&vb%<0M#mP4ODP~GgJAKu#kaz%q%0rUlUoM?yC0HJeXB-_SUNM09D36 zB}Yzw;c--wc2rvR@A%Tl)Z{lZjaL?4u-K$C@4}ar?s|v%rf}{^0d;T32XL?+?_3>uI`CG*tj*IaW zGSyALBeHg$gJqrt+cW{euEhm<&-e|G*`E2nMm$4tQ8c%JT)Fn2!{yb!Y3DOfeC#uF zTJ!t95dTH7%744|zYBI3*tH^Y*Qd6%al8qeqNjX2d0^^NwE{8kzj_l)w=3LVeKut4 z={zwJmCNrB_;pXuXa2dY=tyqv;T=p`M|SGXwm$!I%H^J!j~+}cepGz$fPujt`@@SR z?yl~75qRfOyV&OvmKgc;wNq=4oJ%|+#9e;u%*me&o7YsAYPSff>#je&W@%(YRm4k6 zW`^EXZo*n}Y+-)N=Wks4_g>=5ny}N8FW(J|J2S^l$GqXC%g4$^%o6W^%H7|zS(0@n zD<|8TCjuO5y$79a@?SN6G~NGq1M>>WNd3YydP%0NJFRwJQd4}mJndV=WtCTF1q}6K z-bSBTakV7&5r?9tsr9=r)5ImG9Q)v0b~5#AT!qg^^Oz7X)j89)WgOJ--14ZSk<0ay zI)8d}y!O?Slw&j9_9cqfF-VnpI6eEnAT?Uw=wI0Me3wQ0o){VmRlBOzg`{L9_TO-$ z`}DuP*Sk6OH@Vwq-VdH{Cap3mvt7cTa}+iQ7X-jsH>GG0fXKbtHAqkg$X zr){6JXri-zS(^L@o9g(6S9$nr zHc04My2S1BF}Aq)XtCWk z*|jI_8*^9{L{5Ib?G+{aCon)W#$%$f;?G-*E$?cgB@a&tOVdzro_U)?dqu*XBUg5Z z8@{p26wjKzGG?Lht6D|-A9DRINt-Qx`ab-uv*qpDSBgU0=bn9hP&57hspxfL@n3pe zljizwxpuF6OKB~iqPB7So)dxRjyKIPkSc$bH*`Hu}gYcs-x zi>+t3DTGcdY`QsZ@jBCsg-5r%`=iSj*!t6((jfgm4=*?Wdc~*KvpQ{_a(`TV%C42U%Ue@JiFY53GcUU`1`c8`?N@$R$#SPqe^V)9N(rk|BD)r3=)nl z_@A!(p>K<}msu)nVEO%hrqS&Yk9vFKCmenu_9v_5hPggZbmqb{h3gl`GQ^l=+>PMe zEgNL<_vwj@1~EC&&hqd3yCU8OzphQ5<1f_`bMbHeVbd-DmONe1dFkD^U2}``HYznN zQvZ;0C-YfsXnplMi!UiztO2%bYO=n)mFQdZl8>c?$8_1eDPQ!JzX(SbwEVrg%2ZHv z+2?OZdlK&(i2#`7tDA1?fTK`>lK@cE}{ZO zGK$L|#hl&G@+<9cvlo}L&h!WGPEW{Txc(^S^TK(T1T`*xU8yZ_qT<-~tG=2>Gk$$2 zaEVuRH13)>yK2?)hCjC#_{7fi`p;CQRz1}^4+Bj1=*7^Ne)W_R)sC3Q>Ps?YL z-TK$A%=z0f&rpc@>Kh@q=i83)Cgf&$EZ-of74+bd*7n_hum7yh%aTps@5*Do=;wpQ zjm3*g1g}+p-_6z}q5CQJ#?`_Vh3wlF-|u_7cK0dMvfoWYCl1sE&vfgU@9}rqN~yvN z-1EdWyZQfYH2*b=JGv_O>leqD%CXYj7u#L?i?+KcI!*L@e1A6AZdDn^)jOt0-0HP^ zzy8J3tfQg*?|$CO&*b+?;$6FL^Mu_7$KJoIN%?W$RdUkf?>eCtVT|jpRh?qU(D->^ z%35#x#lO}%%iIk3r#ekxr-S#G{V$xnC!Jo9_T$*rXIk>Vwl8IneDCHS&`@5vs_9@y zs7u*mm5Yy;>whRr`6hBrKG^sR9h)z^<=KIrhNgLu3zZ}O)ra&lG#Lc#KHR9_dw24|_`HvE z^!y)Q@>^8NB>6{EOX1>HoqBbLjm}!JlXnZ-M4k!n+Rw4}lAwhDpSB74A6Mxtof8tE zyJga^2!#*pnr_VTJ9|&%Pp_Qcx2IFX#B4He-A=NJtZWv}6%uO8_4BvesH;8U)|)GJ zQqxzwZ%$PCy(9nRu}{et877+jZwj9N?BL58SGGADR;%)D`fmB@b<`xm(zpwA%>%ZS zM9RNiv*GVn0T!ms@BX+wtmeACnV0z4P&g-hx5-_@+WzJJ zY)&l;nk~%^uHAo1=BZw}ON{N=A1fNxGeXouU(34(7~$R&knoHs{68q!}srF`|{n1mj5!Wb~XQf-_v1M z-1Jy)&Ik34Wv2_*+F!77u`YY^Waj$k$6M!}zi#qw`MotC+@f~O+M&evck}A~L2FGk z1E!?!aj*4asjXsTme7b!PdofoOe=b``_GcvPWQhv;#RMJ*xdP-U*Z1Ji+dtZYTi=V zpda(c>aJQ=QLAUkyqbKjyv{84Q`Joedsgg>-R;!2vp!J2rDC?{Teq))ah-h@3wN)I z>Zyo*estqL_P?@jdsZKlxiCZgRP?%!Cnl-3eV+X_ecjph395~@d>UWa1+B#L9iDdS z7w-wy2vL6i?9Znn`4iUqOsuagED@5Qx+al zq!{d{;I(60=%)ZsPgH3DgvNY$H%A0R>?)HAQns5K9|JSPb%s&}q#lXM- E0I;>$A^-pY literal 0 HcmV?d00001 diff --git a/ds4-color/translations/en.json b/ds4-color/translations/en.json new file mode 100644 index 0000000..949591a --- /dev/null +++ b/ds4-color/translations/en.json @@ -0,0 +1,17 @@ +{ + "ds4_color": "DS4 Color", + "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.", + "settings": { + "glyph": { + "label": "Bar Glyph", + "description": "Icon shown on the bar button." + } + } +} diff --git a/ds4-color/widget.luau b/ds4-color/widget.luau new file mode 100644 index 0000000..33a1f2d --- /dev/null +++ b/ds4-color/widget.luau @@ -0,0 +1,21 @@ +--!nonstrict +local glyph = noctalia.getConfig("glyph") +local tr = noctalia.tr("ds4_color") + +local function render() + barWidget.setGlyph(glyph) + 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/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") +end + +render()