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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: thumbnail

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

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

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

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

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

* Update plugin description for clarity

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

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Lemmy <studio@quadbyte.net>
2026-07-19 08:39:42 -04:00
2026-07-14 23:43:47 -04:00
2026-07-18 01:27:43 -04:00
2026-07-13 21:46:16 +02:00
2026-07-19 00:15:32 -04:00
2026-07-18 19:04:23 -04:00
2026-07-19 00:15:32 -04:00
2026-07-19 00:15:32 -04:00
2026-07-19 00:15:32 -04:00
2026-07-19 00:15:32 -04:00
2026-07-14 23:43:47 -04:00
2026-07-18 01:27:43 -04:00

Community plugins

Noctalia Logo


This repo is the community plugin source for Noctalia. Every plugin merged here is listed in the shell's plugin store and on noctalia.dev/plugins, and users can install it without adding a source of their own.

Plugins maintained by the core team live in official-plugins, which does not accept third-party plugins. This one does. PRs are welcome.

The plugin system is in beta. The manifest format and the plugin API may still change before v5 is stable. Expect to bump your plugin when they do.

Layout

Each plugin is one top-level directory, named after the part of its id that follows the /, so me/hello lives in hello/:

hello/
  plugin.toml             # manifest: id ("me/hello"), metadata, entries, settings
  hello.luau              # your entry scripts
  README.md               # rendered as the plugin's page on noctalia.dev
  thumbnail.webp          # the plugin's card image
  translations/
    en.json               # every label_key / description_key the manifest references

catalog.toml at the repo root indexes every plugin. It is generated by CI, so never edit it or include it in a commit.

A plugin id is <author>/<plugin>. The author part is yours (your GitHub handle is the obvious choice) and keeps your id distinct from everyone else's, but the directory name is first-come within this repo. If weather/ is already taken, pick another name; the official repo is a separate source, so a name used there is not taken here. Both id segments must be lowercase and match [a-z0-9][a-z0-9._-]*.

What a plugin is allowed to be

Noctalia plugins are trusted, unsandboxed Luau. There is no permission broker and no capability sandbox: installing a plugin is equivalent to running a script the user owns. It can read and write files, spawn processes, and talk to the network as the user.

That is a deliberate design choice, and it puts the burden on review. So:

  • No obfuscated, minified, or generated code. A reviewer must be able to read every line you ship.
  • No downloading and executing remote code. Ship your logic in the repo, at a version people reviewed.
  • Declare what you shell out to. External commands go in dependencies in plugin.toml and get a mention in your README.
  • Account for every network call, filesystem write, and spawned process in your PR description.

Anything that looks like it is hiding what it does will be rejected, regardless of intent.

Writing a plugin

The plugin development docs are the reference: the manifest, the entry types ([[widget]], [[panel]], [[shortcut]], [[service]], [[desktop_widget]], [[launcher_provider]]), the declarative UI vocabulary, the runtime API, and the workflow for developing and testing locally.

The fastest start is to read noctalia/example in the official repo. It exercises a bar widget, a declarative widget, a service, a shortcut, a launcher provider, and a panel in one plugin.

To run your plugin while you work on it, add this checkout as a path source:

noctalia msg plugins source add dev path ~/dev/community-plugins
noctalia msg plugins enable me/hello

.luau edits hot-reload; manifest changes are picked up on the next config reload.

Editor setup

noctalia.d.luau declares the whole plugin API, so luau-lsp gives you autocomplete and typo diagnostics. It lives in official-plugins, which is its single source of truth; it is not vendored here, because a committed copy would be a second one for everyone to trust and keep in sync. Fetch it into the repo root, where it is gitignored:

curl -O https://raw.githubusercontent.com/noctalia-dev/official-plugins/main/noctalia.d.luau

Re-run that whenever the plugin API changes; your local copy is a snapshot, not a subscription.

The committed .vscode/settings.json already points luau-lsp at it; for another editor, add it to luau-lsp's types.definitionFiles. .luaurc sets nonstrict mode, matching the --!nonstrict directive every plugin file starts with.

Thumbnail

Every plugin ships a thumbnail.webp. It is the card image in the plugin store and on the website. Generate one with the thumbnail generator: drop in a screenshot of your plugin, set the title, category tag and accent color, then export the 960×540 WebP and commit it as <plugin>/thumbnail.webp.

README

README.md is the plugin's public page, so it must tell a user how to access every entry instead of only describing the implementation. Follow README_TEMPLATE.md, which mirrors the structure used by the official plugins:

  • Start with a title, a short explanation, a Plugin table, and practical Usage instructions.
  • Copy the plugin id and every entry id exactly from plugin.toml.
  • If the plugin declares a panel, include the exact command noctalia msg panel-toggle <author>/<plugin>:<panel-id>.
  • If it declares a launcher provider, document its /<prefix> and give an example query.
  • Mention every manifest dependency under Requirements, using the exact dependency name.
  • Document declared settings, including units or non-obvious effects.
  • Use IPC and Notes when the plugin exposes extra events or has important filesystem, network, process, privacy, hardware, or compositor behavior.

CI derives ids, panel commands, launcher prefixes, dependencies, and whether settings exist from plugin.toml. Its error messages show the exact missing value, while maintainers review the usefulness and accuracy of the prose.

Translations

Write translations/en.json only. Every label_key and description_key in your manifest must resolve to a key in it, and CI checks this. Do not add machine-translated locales; other languages are handled separately.

To test the latest translated locales from Noctalia Translate in a working checkout, run:

./.tools/i18n-pull.sh

The command asks for confirmation and overwrites the locale files returned by the translation service. It does not delete local locale files that are absent from the export. Review the resulting diff before committing anything.

Tags

The tags in plugin.toml are used for catalog search. Tags must be lowercase and selected from this list:

  • Surfaces: bar, desktop, launcher, panel, service, shortcut
  • Purpose: ai, animation, audio, clock, countdown, demo, development, emoticon, fun, gaming, hardware, indicator, language, media, music, network, privacy, productivity, recording, system, theming, time, utility, video, wallpaper
  • Compositors: hyprland, labwc, mangowc, niri, sway
  • Distributions: arch, debian, fedora, gentoo, nixos, opensuse, void

If your plugin does not fit any existing tag, propose a new one in your pull request rather than inventing a tag in the manifest.

Submitting

Open a PR against main. CI validates your manifest, entry scripts, required files, and thumbnail on every push.

  • One plugin per PR.
  • The directory name matches the part of id after the / in plugin.toml exactly.
  • version is semver and gets bumped on every change to the plugin.
  • plugin_api is the oldest Noctalia plugin API level the plugin requires. Use the current documented level for a new plugin, and increase it only when the plugin adopts a capability from a newer API level.
  • description is concise catalog copy, limited to 120 characters. Put feature details in the plugin's README.
  • license is set in plugin.toml. You keep the copyright on your plugin; if it is not MIT, put a LICENSE file in your plugin directory. There is no repo-wide license covering contributed plugins.
  • Screenshots or a short video for anything with a visual surface.

Maintainers read the code before merging. Expect review comments about clarity, and about anything the plugin does that is not obvious from its description.

Maintaining your plugin

The plugin directory is yours. Someone else's PR changing your plugin is not merged without your sign-off, unless it fixes something that is broken or is a mechanical change applied across the whole repo. Maintainers will @-mention you on PRs and issues that touch it.

If you stop maintaining a plugin, set deprecated = true in its plugin.toml rather than deleting the directory. The store keeps working for people who already installed it, but it stops being offered to new users. Plugins that are broken and unmaintained across a Noctalia release may be deprecated by maintainers.

Help

S
Description
No description provided
Readme
16 MiB
Languages
Python 90.9%
Shell 7.2%
Luau 1.6%
Lua 0.3%