Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8406056
docs: design for feature-module (plugin-style) restructure
Jun 28, 2026
a6eee88
refactor(commands): stable Id.NAME type registry so commands can move…
Jun 28, 2026
4453906
refactor(voicemeeter): move command classes into com.getpcpanel.voice…
Jun 28, 2026
4c99990
refactor(obs): move command classes into com.getpcpanel.obs.command
Jun 28, 2026
a14698c
refactor(osc,mqtt): move generic-output commands into feature .comman…
Jun 28, 2026
bdf9a98
refactor(voicemeeter): move VoiceMeeterMuteResolver into the voicemee…
Jun 28, 2026
b0160d2
docs: update feature-module plan with implemented status
Jun 28, 2026
e05cfe4
refactor(commands): decentralize the command type registry (no centra…
Jun 28, 2026
3cfa48c
refactor(commands): split core commands into cohesive feature modules
Jun 28, 2026
3d3cca7
docs: reflect decentralized CommandModule registry + core-command split
Jun 28, 2026
dcc5010
feat(commands): add @CommandMeta — Java source of truth for the assig…
Jun 28, 2026
705601c
feat(commands): generate the frontend command registry from @CommandMeta
Jun 28, 2026
47b2f3a
feat(commands): nice command discriminators, backwards-compatible
Jun 28, 2026
a261ae8
docs: record Java-generated registry + nice backwards-compatible ids
Jun 28, 2026
7f7e121
refactor(rest): delete the dead /api/commands/available registry
Jun 28, 2026
1fcc3f8
refactor(rest): move integration resources into rest/<feature> subpac…
Jun 28, 2026
bdbb689
refactor(icon): contribute VoiceMeeter/OBS command icons via the IIco…
Jun 28, 2026
4472f44
docs(events): add the missing DiscordChangedEvent row
Jun 28, 2026
7218b44
docs: mark feature-module restructure complete
Jun 28, 2026
5c385bb
refactor: move integrations under com.getpcpanel.integration.*
Jun 28, 2026
2422a5b
refactor(rest): move integration REST resources into integration.<fea…
Jun 28, 2026
a08ce3e
refactor(mutecolor): move integration mute resolvers into their featu…
Jun 28, 2026
05bb1ed
docs: integration.* namespace + feature-local REST in the layout
Jun 28, 2026
2a31f33
refactor: move ALL command modules under com.getpcpanel.integration.*
Jun 28, 2026
d4d34da
docs: all command-providing modules under integration.* (engine stays…
Jun 28, 2026
a67e35e
refactor: move per-OS process/window helpers to shared platform.process
Jun 28, 2026
33b72d0
refactor: move the OS audio control facade + backends into integratio…
Jun 28, 2026
b5f319a
refactor: move mute-colour into integration/volume/mutecolor
Jun 28, 2026
26e1cb6
refactor: assemble the rest of the volume feature under integration/v…
Jun 28, 2026
66aab41
refactor: move keyboard + media backends into integration/keyboard/pl…
Jun 28, 2026
d70779d
refactor: assemble the program feature under integration/program
Jun 28, 2026
6be1904
refactor: split hid/ into the device HAL and the command engine (prov…
Jun 28, 2026
b71caa4
refactor: move device-management REST + colour service into the devic…
Jun 28, 2026
2817d21
refactor: fold device/io transports into their provider packages
Jun 28, 2026
bd77b81
refactor: move analog-bands colour service into integration/analogbands
Jun 28, 2026
4fd5ce6
refactor: move integration-specific settings DTOs into their integrat…
Jun 28, 2026
ce26744
fix(native): repoint SndCtrl.dll JNI to SndCtrlNative's new package
Jun 28, 2026
774f3ec
refactor(keyboard): hide platform impls behind a build-selected Keybo…
Jun 28, 2026
077b2fb
fix(native): repoint stale MQTT classNames + reference project intern…
Jun 28, 2026
664579e
refactor: iconextract + Icon/ProcessResource are shared, not program-…
Jun 28, 2026
304dd00
refactor: tighten internal integration classes to package-private
Jun 28, 2026
8d68749
refactor: give util/ structure — group loose helpers into subpackages
Jun 28, 2026
f875097
docs: bring CLAUDE.md + feature-module-structure.md to the final stru…
Jun 28, 2026
ed9e5c0
ci: add a manual Windows workflow to build SndCtrl.dll as an artifact
Jun 28, 2026
174b0b8
refactor(device): move PCPanel device models + factory into the pcpan…
Jun 28, 2026
cac402a
refactor(device): move ProVisualColorsService into the pcpanel provider
Jun 28, 2026
7e14388
refactor(device): move BrightnessService to integration/device with i…
Jun 28, 2026
e7796f3
refactor(device): move the PCPanel DeviceType enum into the pcpanel p…
Jun 28, 2026
84c0c32
fix(test): make CommandRegistryGeneratorTest robust to CRLF on Windows
Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
43 changes: 43 additions & 0 deletions .github/workflows/build-sndctrl-dll.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Build SndCtrl.dll (Windows)

# Manual, on-demand build of the Windows native audio library SndCtrl.dll with MSVC.
# It ONLY builds the DLL and holds it as a workflow artifact — it does not attach it to a
# release or commit it. Download the artifact and commit src/main/resources/SndCtrl.dll by hand
# after changing the C++ sources under src/main/cpp/ (e.g. when SndCtrlNative's package changes).
on:
workflow_dispatch:

jobs:
build:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4

# The build needs the Windows JNI headers (include/ + include/win32/jni_md.h), nothing more.
- name: Set up JDK (for JNI headers)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Configure (CMake + MSVC)
shell: pwsh
run: cmake -S src/main/cpp -B build -G "Visual Studio 17 2022" -A x64 "-DWIN_JDK_HOME=$env:JAVA_HOME"

- name: Build (Release)
shell: pwsh
run: cmake --build build --config Release

# Log the exported JNI entry points so a package mismatch is visible in the run output.
# Best-effort: dumpbin is only on PATH inside a VS dev shell, so never fail the build on it.
- name: Show exported JNI symbols
shell: pwsh
continue-on-error: true
run: dumpbin /exports build\Release\SndCtrl.dll | Select-String "Java_|JNI_"

- name: Upload SndCtrl.dll
uses: actions/upload-artifact@v4
with:
name: SndCtrl-dll
path: build/Release/SndCtrl.dll
if-no-files-found: error
80 changes: 49 additions & 31 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,20 @@ injection, and cross-cutting communication uses the **CDI event bus** (`jakarta.
fire + `@Observes`) heavily rather than direct calls. `docs/events.md` catalogs the events with their
firers and observers — keep it current when you add or remove an event.

**Hardware path (`hid/`, `device/`):** `DeviceScanner` discovers HID devices via hid4java;
`DeviceCommunicationHandler` (one per device, own thread + queue) reads knob/button input and writes
RGB/output. `Device` subclasses (`PCPanelMini/Pro/RGB`) model each hardware variant. Physical input
**Hardware path (`device/`):** the device layer is the hardware-abstraction layer (HAL) and is **not**
an integration — it provides no commands. `DeviceScanner` (in `device/provider/pcpanel/`) discovers HID
devices via hid4java; `DeviceCommunicationHandler` (one per device, own thread + queue, same package)
reads knob/button input and writes RGB/output. `Device` subclasses (`PCPanelMini/Pro/RGB`, in `device/`)
model each hardware variant; `DeviceHolder` (in `device/`) is the cross-provider registry. Physical input
becomes a `PCPanelControlEvent` / `ButtonClickEvent` on the event bus.

**Device providers (`device/provider/`, `device/descriptor/`):** the device layer is generalized so
PCPanel is one `DeviceProvider` among several — providers are `@ApplicationScoped` beans discovered via
`Instance<DeviceProvider>` (NOT build-time stereotypes; every build contains all of them).
`DeviceScanner` is the `"pcpanel"` HID provider; `DeejSerialProvider` (serial, jSerialComm) and
`MidiProvider` (`javax.sound.midi`) are external providers. A device is described by a data
`DeviceScanner` is the `"pcpanel"` HID provider (`device/provider/pcpanel/`); `DeejSerialProvider`
(serial, jSerialComm, `device/provider/deej/`) and
`MidiProvider` (`javax.sound.midi`, `device/provider/midi/`) are external providers; each provider
absorbs its own IO transport (e.g. `SerialTransport`/`JSerialComm*` under `deej/`). A device is described by a data
`DeviceDescriptor` (analog/digital inputs with source ranges, light/analog outputs, capabilities)
rather than the `DeviceType` enum, which is now PCPanel-provider-internal. Each provider normalizes
its raw analog values to the canonical **0–255** internal domain at its edge (PCPanel RGB 0–100, Deej
Expand All @@ -105,15 +109,21 @@ paths (lighting, `OutputInterpreter.sendInit`) against `deviceType() == null`. `
`PcDeviceComponent` for PCPanel, else `GenericDeviceComponent`). Full design + per-phase status:
`docs/device-layer-generalization-plan.md`.

**Command model (`commands/`):** A user's per-dial/button configuration is a `Commands` (list of
`Command` subclasses in `commands/command/` — e.g. `CommandVolumeProcess`, `CommandKeystroke`,
`CommandObs`, `CommandMedia`). `CommandDispatcher` maps incoming control events to the configured
commands and executes them. Commands are JSON-polymorphic and are part of the generated TS contract.

**Native audio abstraction (`cpp/`):** `ISndCtrl` is the OS-audio facade (volume/mute/default device,
focus app). Implementations are selected at **build time** by platform stereotypes:
`@WindowsBuild` (`SndCtrlWindows` → JNI to `SndCtrl.dll` via `SndCtrlNative`, source in
`src/main/cpp/`) and `@LinuxBuild` (`SndCtrlPulseAudio` via JNA/PulseAudio). These stereotypes wrap
**Command model (`commands/` = engine; `integration/*/command/` = the commands):** A user's
per-dial/button configuration is a `Commands` (list of `Command` subclasses). `commands/` holds only the
engine — `Command`, the `Dial/Button/DeviceAction` SPIs, `CommandDispatcher`, `DialValue`, the
`@CommandMeta`/`CommandModule` registry. Each concrete command lives in its feature's package, e.g.
`integration.volume.command.CommandVolumeProcess`, `integration.keyboard.command.CommandKeystroke`/
`CommandMedia`, `integration.obs.command.CommandObs`. Commands are JSON-polymorphic (`@JsonTypeName`
ids, decentralized registry — see `docs/feature-module-structure.md`) and part of the generated TS
contract. **A package is an `integration` only if it provides commands** — providers/HAL/infra are not.

**Native audio abstraction (`integration/volume/platform/`):** `ISndCtrl` is the OS-audio facade
(volume/mute/default device, focus app) — it is the backend the volume commands drive, so it lives in
the volume feature. Implementations are selected at **build time** by platform stereotypes:
`@WindowsBuild` (`SndCtrlWindows` → JNI to `SndCtrl.dll` via `SndCtrlNative`, both in
`integration/volume/platform/windows/`; C++ source in `src/main/cpp/`) and `@LinuxBuild`
(`SndCtrlPulseAudio` in `platform/linux/`, via JNA/PulseAudio). These stereotypes wrap
Quarkus `@IfBuildProperty(name="pcpanel.build.os", ...)` keyed off `pcpanel.build.os` (set at build
time from `os.detected.name`), so **a given build only contains one platform's beans** — guard
optional platform beans with `Instance<T>` injection, and use `CdiHelper` to fetch beans from
Expand All @@ -129,11 +139,14 @@ and immutable distros persist settings without a host grant. `PcPanelRoot.resolv
source of truth: `Main` publishes it as the `pcpanel.root` system property for the native image, and
the non-CDI `FileChecker`/`HidDebug` call it directly. See `linux.md` for the user-facing details.

**Frontend bridge (`rest/`):** JAX-RS resources under `/api` (`DeviceResource`, `CommandsResource`,
`SettingsResource`, …) plus a single websocket `EventWebSocket` at `/ws/events`. The backend pushes
device/state snapshots (DTOs in `rest/model/`) to the Angular UI over the socket; `EventBroadcaster`
fans CDI events out to connected clients. There is no separate window framework — the "UI" is the
browser served by Quinoa.
**Frontend bridge (`rest/`):** the **shared** JAX-RS + websocket bridge: `SettingsResource`,
`PlatformResource`, `SystemResource`, `IconResource`/`ProcessResource` (the app/process picker, shared
across features), `EventWebSocket` at `/ws/events`, `EventBroadcaster`, `LocalHttpGuard`, and the
`rest/model/` DTO+WS contract. Feature-specific resources live with their feature instead:
device-management REST in `device/rest/` (`DeviceResource`, `SerialResource`, `MidiResource`),
volume/overlay REST in `integration/volume/`, each external connector's REST in `integration/<name>/rest/`.
The backend pushes device/state snapshots to the Angular UI over the socket. There is no separate window
framework — the "UI" is the browser served by Quinoa.

**Web-exposure security model:** the API is unauthenticated, so it must stay reachable only from the
local machine. Two layers enforce this: `quarkus.http.host=127.0.0.1` keeps other hosts off, and
Expand All @@ -146,9 +159,13 @@ the backstop. Toggle with `pcpanel.http.local-only` (default true). This does **
*local* callers — defending against other processes on the same machine would need a token and is out
of scope.

**Integrations:** `obs/` (OBS websocket), `voicemeeter/` (JNA), `wavelink/` + `dev/niels/wavelink/`
(Elgato Wave Link RPC client), `osc/`, `mqtt/` (Eclipse Paho mqttv5), `homeassistant/`. `overlay/`
draws an on-screen volume overlay: a Win32 JNA layered window on Windows (`Win32VolumeOverlay`) and a
**Integrations (`integration/*` — command-providing features only):** the external connectors
`integration/obs/` (OBS websocket), `voicemeeter/` (JNA), `wavelink/` + `dev/niels/wavelink/` (Elgato
Wave Link RPC client), `osc/`, `mqtt/` (Eclipse Paho mqttv5), `homeassistant/`, `discord/`; plus the
feature families `volume/`, `keyboard/`, `program/`, `analogbands/`, `profile/`, and `device/` (the
brightness command only). Each owns its `command/` + `CommandModule` and (where applicable) its REST,
SPI impls, and service. The on-screen volume overlay lives in `integration/volume/overlay/`: a Win32 JNA
layered window on Windows (`Win32VolumeOverlay`) and a
desktop-drawn OSD over D-Bus on Linux/Wayland (`LinuxOverlay`, AWT-free) — KDE Plasma's native volume
OSD (`org.kde.osdService.volumeChanged`, the same real-time bar as Plasma's own volume keys) when
plasmashell is on the bus, **else no overlay** (clean no-op). A notification fallback was deliberately
Expand All @@ -158,11 +175,11 @@ so those `Save` settings don't apply on Linux (the settings UI greys them out).
Selection is the runtime `Platform` check in `Overlay.createOverlay()`.
`util/tray/` is the system tray (Wayland uses the D-Bus StatusNotifierItem protocol via dbus-java).

`homeassistant/` is *outbound* control (the app drives Home Assistant), distinct from the MQTT
auto-discovery in `mqtt/` (which lets Home Assistant discover the app). It holds both its own command
types (`homeassistant/command/`, kept off the big `commands/command/` pile per the action-package
convention — Jackson `Id.CLASS` makes location irrelevant, but the typescript-generator needs the
`com.getpcpanel.homeassistant.command.**` classPattern in `pom.xml`) and a minimal REST client
`integration/homeassistant/` is *outbound* control (the app drives Home Assistant), distinct from the
MQTT auto-discovery in `integration/mqtt/` (which lets Home Assistant discover the app). It holds both
its own command types (`integration/homeassistant/command/` — every feature's commands live in its own
`command/` package, all picked up by the single `com.getpcpanel.**.command.**` typescript-generator
classPattern in `pom.xml`) and a minimal REST client
(`HomeAssistantClient`, JDK `HttpClient`, no extra dependency). Multiple servers are configured in
settings (`Save.homeAssistantServers`); a command with a blank server id auto-resolves to the only
configured server (the UI also auto-selects it). **Actions are authored as pasted HA "action" YAML**
Expand Down Expand Up @@ -197,7 +214,8 @@ Key constraints baked into those args, change with care:
`quarkus-awt` is dropped via the `os-non-mac` profile, and the `os-mac` profile defers the whole
AWT/Java2D/Swing/ImageIO subsystem to run-time). So macOS must never *call* AWT: the overlay is a
no-op (`NoOpOverlayWindow`), icons are disabled, keystrokes use CoreGraphics `CGEvent`
(`com.getpcpanel.cpp.osx.OsxKeyboard`), the tray and `java.awt.Desktop` are skipped. JNA classes
(`com.getpcpanel.integration.keyboard.platform.osx.OsxKeyboard`, the macOS `Keyboard` impl), the tray
and `java.awt.Desktop` are skipped. JNA classes
that run `Native.load` in their initializer must be `--initialize-at-run-time` (narrowly, by class —
a package-wide directive would wrongly catch Quarkus's build-time CDI `_Bean` objects).
- Windows-only GUI-subsystem linker flags (`/SUBSYSTEM:WINDOWS`, `/ENTRY:mainCRTStartup`) are MSVC
Expand Down Expand Up @@ -298,9 +316,9 @@ Full reference: [`docs/mcp-server.md`](docs/mcp-server.md).
## Conventions

- Lombok is used throughout (`@Data`, `@Log4j2`, `@RequiredArgsConstructor`, etc.). `@Log4j2` is the
logging annotation (backed by jboss-logmanager). Note the `cpp` package overrides this with
**fluent** accessors (`src/main/java/com/getpcpanel/cpp/lombok.config`): `AudioDevice`/`AudioSession`
use `name()`/`volume()`/`muted()`, not `getName()`.
logging annotation (backed by jboss-logmanager). Note the audio-facade package overrides this with
**fluent** accessors (`src/main/java/com/getpcpanel/integration/volume/platform/lombok.config`):
`AudioDevice`/`AudioSession` use `name()`/`volume()`/`muted()`, not `getName()`.
- Nullability annotated with `javax.annotation.@Nullable/@Nonnull` (JSR-305) — these feed the TS
generator's optional-property detection.
- `.editorconfig` defines formatting and a large set of IntelliJ inspection settings; follow it.
Expand Down
1 change: 1 addition & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ up to date when you add or remove an event or an observer.
| `VoiceMeeterDirtyEvent` | `Voicemeeter` | `VoiceMeeterMuteService` |
| `VoiceMeeterMuteEvent` | `VoiceMeeterMuteService` | `VoiceMeeterMuteResolver` (mute-colour) |
| `WaveLinkChangedEvent` | `WaveLinkService` (Wave Link state incl. mute changed) | `MuteColorService` |
| `DiscordChangedEvent` | `DiscordService` (Discord voice/mute/deafen state changed) | `MuteColorService` |
| `MuteOverridesDirtyEvent` | mute-colour resolvers (e.g. `VoiceMeeterMuteResolver` after caching a mute change) | `MuteColorService` |
| `MqttStatusEvent` | `MqttService` | `MqttDeviceService` |

Expand Down
Loading
Loading