refactor: feature-module (plugin-style) restructure#111
Merged
Conversation
Analysis of the current command/integration scatter and a target structure where each integration is a self-contained module discovered via CDI + annotations. Captures the Id.CLASS persisted-type constraint, the ~20 registries an integration command touches today, and a phased migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… package Switch Command polymorphism from Jackson Id.CLASS to Id.NAME + an explicit @JsonSubTypes registry. Each subtype's name is a stable, location-independent string equal to the class's historical FQCN — so saved profiles.json, the generated TypeScript _type union, and the frontend command catalog are all unchanged, while a command class is now free to move into its own feature package (e.g. com.getpcpanel.voicemeeter.command) without breaking any of them: only the @type(value=…) reference moves, the name stays frozen. This is the seam that lets each integration own its command classes. - CommandSubtypeRegistryTest guards the two invariants: every concrete Command subtype is registered exactly once (the 'forgot to register' guard), and every persisted id still deserializes to its class (the save-migration guard). - typescript-generator now emits a CommandUnion tagged union (it keys off @JsonSubTypes); control.component.ts casts its two loose command-build sites to CommandUnion. backend.types.ts is otherwise unchanged except abstract base classes correctly drop out of the concrete _type literal unions. Pre-existing FocusVolumeOverrideServiceTest failures (3, env-specific focus detection) are unrelated and unchanged by this commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…meeter.command git-mv the five CommandVoiceMeeter* classes from the shared commands/command pile into the VoiceMeeter module, matching the WaveLink/Discord/HomeAssistant convention. Enabled by the stable Id.NAME registry: the persisted _type names stay the historical FQCN, so saved profiles.json, the generated TypeScript _type union, and the frontend command catalog are unchanged (the union diff is a pure reordering of the same literal set). - @JsonSubTypes / CommandConverter / NativeImageConfig / IconService / CommandsResource / VoiceMeeterConnectedVolumeService / VoiceMeeterMuteResolver updated to import from the new package. - reachability-metadata.json: stale VoiceMeeter FQCNs repointed. - pom typescript-generator: collapsed the per-feature command classPatterns to one glob com.getpcpanel.**.command.** so future feature command packages are picked up with no build-config edit. History preserved via git mv. ReflectionRegistrationCoverageTest and the new CommandSubtypeRegistryTest stay green; frontend tsc unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same pattern as the VoiceMeeter move: git-mv the CommandObs* classes out of the shared commands/command pile into the OBS module. Persisted _type names stay the historical FQCN (frozen via @JsonSubTypes), so saves, the TS _type union, and the frontend catalog are unchanged. Importers (@JsonSubTypes, CommandConverter, NativeImageConfig, IconService, ObsMuteResolver, ObsConnectedVolumeService, CommandsResource) and reachability-metadata repointed. classPatterns glob already covers obs.command. Guards green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d packages git-mv CommandOscSend -> com.getpcpanel.osc.command and CommandMqttPublish -> com.getpcpanel.mqtt.command. Both extend the generic CommandValueOutput base (which stays in commands/command). Frozen @JsonSubTypes names keep saves/TS/UI unchanged; NativeImageConfig + @JsonSubTypes imports repointed. Guards green. With this, all integration commands (VoiceMeeter, OBS, OSC, MQTT — plus the already-modular WaveLink/Discord/HomeAssistant) live in their feature's .command package; commands/command now holds only the engine + genuinely-core commands (volume/media/keystroke/run/brightness/profile/HTTP/value-output). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ter module The VoiceMeeter-specific mute resolver lived in the generic mutecolor package and exposed its VM_PATTERN regex for NamedDeviceMuteResolver to reach into. Move it to com.getpcpanel.voicemeeter; it implements the shared MuteStateResolver SPI so it is still discovered by MuteColorService via @ALL List<MuteStateResolver> regardless of package. NamedDeviceMuteResolver now imports it; the resolver imports the mutecolor SPI + dirty-event types. MuteColorServiceTest stays green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the frozen-FQCN-id foundation, the four command relocations + the VoiceMeeterMuteResolver move as done, and the remaining work (annotation-driven catalog + pretty ids, resource/icon/settings consolidation, dead-code + events.md cleanup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l list) Replace the central @JsonSubTypes block on Command with fully decentralized, per-package self-registration so adding a command (or a whole plugin) never touches anything outside its own feature package: - Command keeps @JsonTypeInfo(Id.NAME) but drops @JsonSubTypes entirely. - Each concrete command declares its own stable id via @JsonTypeName in its own file (the id stays the historical FQCN, so saves/TS/frontend are unchanged). - New CommandModule CDI SPI: each feature has an @ApplicationScoped module that lists only its own commands. CommandSubtypeRegistrar (an ObjectMapperCustomizer) collects them via @ALL and registers them with Jackson — mirroring the existing @All-discovered SPIs (MuteStateResolver, IIconHandler, DeviceProvider). - typescript-generator emits the _type union from @JsonTypeName (verified): the generated literal set is byte-identical, CommandUnion is no longer synthesised (control.component reverts its two casts to Command), WsEventUnion is unaffected. Tests: CommandSubtypeRegistryTest now guards the decentralized invariants (every command self-identifies with a unique @JsonTypeName; the CommandModule SPI covers exactly the concrete set; every id resolves). CommandSubtypeRegistrarTest checks the registrar wiring. CommandKeystrokeTest registers its subtype on its bare mapper. CoreCommandModule is a temporary single module for the still-shared core commands; the next commits split it into per-family modules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
commands/command was still a catch-all mixing unrelated families (volume vs shortcuts vs media...). Split every core family into its own self-contained module via git mv, each registering itself through the CommandModule SPI — so commands/command now holds ONLY the engine (Command, CommandNoOp, CommandConverter, the Dial/Button/DeviceAction SPIs, EngineCommandModule). New core modules (package + commands + a CommandModule bean): volume.command — all CommandVolume* (process/focus/device/default-device…) keyboard.command — CommandKeystroke, CommandMedia program.command — CommandRun, CommandShortcut, CommandEndProgram device.command — CommandBrightness profile.command — CommandProfile analogbands.command — CommandAnalogBands (+ AnalogBand) output.command — CommandHttpRequest (+ CommandValueOutput base) Frozen @JsonTypeName ids keep the persisted _type unchanged, so saves, the TS _type union (verified byte-identical literal set), and the frontend are unaffected by the moves. The classPatterns glob and the CommandModule SPI mean no central file changed. Importers (CommandConverter, NativeImageConfig, etc.) and reachability-metadata repointed; CommandAnalogBandsTest moved next to its subject. Full suite green (only the 3 pre-existing FocusVolume failures); tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nable-command registry Annotate the 39 user-assignable commands with @CommandMeta(label, category, kinds, integration, icon) in com.getpcpanel.commands.meta — the picker/registry metadata that is currently hand-maintained in the frontend command-catalog.ts. Values mirror the catalog exactly (verified). Purely additive: nothing consumes the annotation yet; the next commit adds a build-time generator that emits the frontend command registry from these annotations, so a command's metadata lives in Java next to the command, in its own feature package. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hand-maintained command-level registry in command-catalog.ts (label, category, kinds, integration, icon per command) is now generated from the Java @CommandMeta annotations into command-registry.generated.ts. command-catalog.ts consumes it and keeps only the field editors (buildEmpty + fields[]), which are Angular UI, not a registry. So 'which commands exist and how they're classified' is retrieved from the Java code, per command, in its own feature package. - CommandRegistryGeneratorTest emits the file and guards staleness (regenerate with -Dpcpanel.generate.catalog); it also asserts every @CommandMeta is a concrete command with a @JsonTypeName id. - COMMANDS is assembled by joining the generated metadata with the hand-written field schemas by type id. Values are identical to the previous hand catalog (verified), so frontend behaviour is unchanged; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the persisted _type of the 39 assignable commands from their frozen FQCN to a readable id (e.g. com.getpcpanel.commands.command.CommandVoiceMeeterAdvanced -> voicemeeter.advanced) via @JsonTypeName, while staying backwards compatible: - @CommandMeta.legacyIds records each command's previous id. CommandSubtypeRegistrar registers the current (nice) id as the subtype and installs a Jackson DeserializationProblemHandler that maps an unknown legacy id back to its command on READ only. So old profiles.json keep loading; re-saving writes the nice id — a transparent one-way conversion. New ids are never ambiguous with old ones for serialization (the handler is deser-only). - The generated registry carries so the frontend joins its hand-written field schemas (keyed by the old id) without churn, and stamps the nice _type in buildEmpty. - CommandSubtypeRegistrarTest covers both directions: nice id loads, legacy FQCN loads, and a re-save converts to the nice id. typescript-generator now emits the nice _type literals; tsc clean; full suite green (only the 3 pre-existing FocusVolume failures). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CommandsResource (and its CommandType DTO) was a stale, non-authoritative command list the frontend never called — the frontend command catalog is the real registry, now generated from @CommandMeta. Remove both and prune their reachability-metadata entries. backend.types.ts drops the unused CommandType interface; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kages VoiceMeeterResource -> rest/voicemeeter, ObsResource -> rest/obs, OscResource -> rest/osc, matching the rest/wavelink + rest/discord layout. @path is unchanged so the URLs (/api/voicemeeter, /api/obs, /api/osc) are identical; reachability-metadata repointed (the quarkusrestinvoker hash is method-derived, so only the package prefix changes). NOTE: rewrites Quarkus native-image REST-invoker metadata, fully verifiable only by a native build (per CLAUDE.md); JVM build + coverage tests are green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nHandler SPI Replace the two hardcoded imageHandlers.put(CommandObs/CommandVoiceMeeter,...) entries in IconService with ObsIconHandler + VoiceMeeterIconHandler @ApplicationScoped beans (mirroring WaveLinkIconHandler), discovered via @ALL List<IIconHandler> — so a feature's command icon lives in its package. Behaviour identical (same images); IconService.OBS/VOICEMEETER made public like DEVICE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
External-connector integrations are now grouped under a dedicated namespace rather
than sitting at the top level next to core/platform packages:
obs, voicemeeter, wavelink, discord, homeassistant, mqtt, osc
-> com.getpcpanel.integration.{obs,voicemeeter,wavelink,discord,homeassistant,mqtt,osc}
Each integration keeps everything it owns (its .command subpackage, service/engine,
icon handler, mute resolver, events). Core command families (volume/keyboard/program/
device/profile/analogbands/output) are not integrations and stay put.
Persisted ids are unaffected: the nice @JsonTypeName ids and the frozen legacyIds
(old FQCN aliases) are location-independent, so saves/TS/frontend don't change (the
backend.types.ts diff is a pure reorder of the same _type literal set). Imports
rewritten codebase-wide (import-anchored, so legacyIds string literals are preserved);
native-image reachability/proxy-config + the VoicemeeterInstance init-at-run-time args
(pom + application.properties, parity-locked) repointed. classPatterns glob
com.getpcpanel.**.command.** still matches. Full suite green; tsc clean.
Native-image REST/JNA wiring is fully verified only by the native CI build (per CLAUDE.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ture>.rest Answers 'why rest/discord not discord/rest': the per-feature REST resources + DTOs now live with their feature (integration.discord.rest, integration.obs.rest, …) instead of grouped by layer under rest/. This matches what HomeAssistant already did (homeassistant.rest) and the feature-locality principle used everywhere else. The shared web bridge (LocalHttpGuard, EventWebSocket, EventBroadcaster, rest/model) stays in rest/. @path unchanged so URLs are identical; **.dto.** glob still generates the DTO types; reachability-metadata + proxy-config repointed. Full suite green; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re packages DiscordMuteResolver -> integration.discord, ObsMuteResolver -> integration.obs, WaveLinkMuteResolver -> integration.wavelink (matching VoiceMeeterMuteResolver, already in its package). They implement the shared MuteStateResolver SPI so MuteColorService still discovers them via @ALL regardless of package. mutecolor/ now holds only the orchestrator + SPI + integration-agnostic resolvers (process/device/named-device). Full suite green; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the principle 'if it provides commands, it's an integration', the core command
families now live alongside the external connectors under integration.*:
volume, keyboard, program, device, profile, analogbands, output (.command)
-> com.getpcpanel.integration.{...}.command
com.getpcpanel.commands is the command ENGINE only (Command, CommandNoOp,
CommandConverter, the Dial/Button/DeviceAction SPIs, CommandValueOutput shared base,
the @CommandMeta/CommandModule registry SPI). CommandValueOutput moved here from
output/ since http/osc/mqtt all extend it.
Non-command infra stays put: volume audio services (used by overlay/rest), the
device-provider framework, profile persistence, the analog-bands colour service.
Persisted ids are location-independent (nice @JsonTypeName + frozen legacyIds), so
saves/TS/frontend are unaffected (backend.types.ts diff is a pure reorder of the same
_type set). Imports rewritten import-anchored (legacyIds string literals preserved);
reachability-metadata repointed. classPatterns glob com.getpcpanel.**.command.** still
matches all of them. Full suite green; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in commands/) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The foreground-window/process-lookup helpers were scattered across cpp/{windows,
linux,osx} but are consumed by three unrelated features — the volume audio
backends (SndCtrlPulseAudio/SndCtrlOsx inject them for focus-volume + session
enumeration), the program commands (IPlatformCommand), and Discord screen-share
(via IProcessHelper). By the 3+-unrelated-consumers rule they are shared infra,
not feature-owned, so they move next to their IProcessHelper SPI rather than into
any single integration.
Moves WindowsProcessHelper, LinuxProcessHelper, OsxProcessHelper and
ProcessConditionalHelper (Linux-internal) into com.getpcpanel.platform.process;
IProcessHelper stays in com.getpcpanel.platform. Pure-Java move — no native-image
config touched. Done first so the volume audio backends move onto a stable target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n/volume/platform Dissolves the audio half of the cpp/ grab-bag. The ISndCtrl facade and its shared value types (AudioDevice, AudioSession, AudioDevice/SessionEvent, DataFlow, EventType, MuteType, Role) move to com.getpcpanel.integration.volume.platform; the three per-OS backends move into windows/ (SndCtrl + WindowsAudio* + the internal ProcessHelper), osx/ (SndCtrlOsx + OsxAudioDevice + CoreAudio*) and linux/ (SndCtrlPulseAudio + PulseAudio*). Audio is the backend the volume commands drive, so it belongs with the volume feature. - The cpp/ fluent-accessor lombok.config (name()/volume()/muted()) is replicated at the platform/ package root so the audio data classes keep their fluent API. - WindowFocusChangedEvent moves to com.getpcpanel.profile: it is fired by the Windows audio backend but is consumed only for profile-switching-on-focus (ProfileWindowFocus Service + DeviceHolder), so leaving it under volume would couple volume<->profile. - ProxyRegistrationCoverageTest (a global JNA-proxy guard scanning com.getpcpanel.**) moves to com.getpcpanel.graalvm next to NativeImageConfig. - Native-image config repointed in lockstep: jni-config.json (AudioDevice/Session, SndCtrlWindows, WindowsAudioDevice), proxy-config.json + NativeImageConfig (CoreAudioLib + nested), the CoreAudioLib --initialize-at-run-time entries in BOTH pom.xml and application.properties, and every reachability-metadata.json audio entry. Verified: JVM build green; NativeBuildArgsParity, ReflectionRegistrationCoverage, ProxyRegistrationCoverage, SndCtrlNativeConfig + the PulseAudio tests all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mute-colour is the volume feature's per-control LED layer: DeviceMuteResolver/ ProcessMuteResolver/NamedDeviceMuteResolver query ISndCtrl and MuteColorService observes the audio device/session events. The MuteStateResolver SPI, the orchestrator, the three ISndCtrl-backed resolvers and MuteOverridesDirtyEvent move to com.getpcpanel.integration.volume.mutecolor. Per-integration resolvers (Voicemeeter/ OBS/Discord/WaveLink) keep contributing via @ALL from their own packages — only their import of the SPI repoints. reachability-metadata MuteColorService entry repointed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olume Pulls the remaining volume-owned code into the feature package: - volume/ leftovers (VolumeCoordinatorService, FocusVolumeOverrideService, IFocusRedirector, LinuxNewSessionVolumeService) -> integration/volume - overlay/ (the whole on-screen volume HUD incl. Win32/Linux/NoOp impls, renderers, KdeOsdService, OverlayDemoTrigger) -> integration/volume/overlay - the volume-facing REST surfaces (AudioResource, OverlayResource, FocusVolumeDiagnosticsResource) -> integration/volume; rest/ keeps the shared EventBroadcaster/EventWebSocket/SettingsResource relay - the focus/overlay persisted DTOs FocusVolumeOverride, FocusVolumeTarget -> integration/volume and OverlayPosition -> integration/volume/overlay; Save keeps the fields and imports them from there Native-image config repointed in lockstep: KdeOsdService (proxy-config + reflect- config), the OverlayRenderer --initialize-at-run-time entry in pom.xml + application. properties, NativeImageConfig's FocusVolume*/OverlayPosition imports, and the reachability-metadata entries for the overlay, the two REST resources (quarkusrest invoker hashes preserved — they are method-derived), VolumeCoordinatorService and OverlayPosition. Redundant same-package imports created by the move were stripped. Verified: JVM build green; parity/coverage guards + overlay + focus-volume tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atform — cpp/ is gone Completes the dissolution of the cpp/ grab-bag. The per-OS keystroke/media-key backends move under the keyboard feature: - WindowsKeyboard -> integration/keyboard/platform/windows - OsxKeyboard, OsxMediaControl (from util/) -> integration/keyboard/platform/osx - LinuxKeyboard, LinuxMprisMediaControl, MprisPlayer -> integration/keyboard/platform/linux They are consumed by KeyMacro (engine) and CommandMedia (keyboard integration); the Linux MPRIS + macOS media controls are the media-key backends peer to the keyboards, so they sit beside them per platform. With this the com.getpcpanel.cpp package — and its fluent-accessor lombok.config, which only the audio data classes needed — is removed entirely. Native-image config repointed in lockstep: LinuxKeyboard$X11/$XTest --initialize-at- run-time in BOTH pom.xml and application.properties + reachability-metadata; OsxKeyboard$CoreGraphics/$CoreFoundation in pom/app.properties + proxy-config; MprisPlayer in reflect-config + proxy-config. The keyboard *NativeConfigTest + *KeystrokeTest suites moved with their subjects. Verified: JVM build green; parity/coverage guards + all keyboard keystroke/native- config tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-locates the program/process-control feature's code: - iconextract/ (the IIconService SPI + the Windows/Linux/Mac impls + the Windows JNA/COM backend chain JIconExtract -> Shell32Extra/IShellItemImageFactory/SIZEByValue) -> integration/program/iconextract. App-icon extraction serves the program feature's process/app picker. The commands/IconService wrapper stays in the engine. - util/IPlatformCommand (per-OS exec/kill orchestrator) -> integration/program; its only consumers are the program commands CommandRun/CommandShortcut/CommandEndProgram. - rest/IconResource + rest/ProcessResource -> integration/program (the app/process + icon REST surfaces feeding the picker). - util/ShortcutHook -> com.getpcpanel.profile: despite the "shortcut" name it maps global shortcuts to PROFILE activation (imports Profile/SaveService, observes SaveEvent), so it belongs with the other focus/shortcut-driven profile-switching code, not program. Native-image config repointed in lockstep: Shell32Extra (proxy-config + the --initialize- at-run-time entry in BOTH pom.xml and application.properties), and the reachability- metadata entries for the icon SPI/impls, IPlatformCommand (+ $WindowsPlatformCommand), ShortcutHook, and the two REST resources (quarkusrestinvoker hashes preserved). Verified: JVM build green; parity/coverage guards + IconExtractNativeTest pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iders are NOT integrations) A device provider supplies hardware input, not commands, so it belongs in the device hardware-abstraction layer — never under integration/ (which is for command-providing modules only). hid/ is dissolved accordingly: - The PCPanel HID provider — DeviceScanner (the "pcpanel" DeviceProvider), DeviceCommunicationHandler(+Factory), InputInterpreter, OutputInterpreter, ByteWriter, ButtonClickEvent, HidDebug — moves to com.getpcpanel.device.provider.pcpanel, beside the deej/ and midi/ providers already in the HAL. - DeviceHolder -> com.getpcpanel.device: the cross-provider device registry (19 consumers), core HAL infra. - BrightnessService + (next commit) the device colour service -> com.getpcpanel.device: they are injected by the HAL's OutputInterpreter, so they must live in the HAL, not in an integration the HAL would then depend upward on. - DialValue + DialValueCalculator -> com.getpcpanel.commands (the engine value model). com.getpcpanel.integration.device keeps ONLY the brightness command module (CommandBrightness + DeviceCommandModule) — that genuinely provides a command. reachability-metadata repointed for all moved provider classes + nested events + tests. Verified: JVM build green; parity/coverage guards + DialValue(+Calculator)/DeviceHolder Backfill/Brightness/InputInterpreter/ByteWriter tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e layer These provide no commands, so they are not integrations — they are the device subsystem's own surfaces: - DeviceResource, SerialResource, MidiResource (device lifecycle/profiles/assignments + deej/midi provider orchestration) -> com.getpcpanel.device.rest - ProVisualColorsService (PCPanel-Pro LED colour computation, consumed by the HAL's OutputInterpreter, the analog-bands colour service, mute-colour and the device snapshot DTO) -> com.getpcpanel.device reachability-metadata repointed (the REST resources keep their method-derived quarkusrestinvoker hashes). The shared rest/ bridge keeps EventBroadcaster/EventWebSocket/ SettingsResource and the model/ TS+WS contract. Verified: JVM build green; parity/coverage guards + ProVisualColorsServiceTest pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The IO transports had exactly one provider consumer each, so co-locate them: SerialTransport, JSerialCommTransport and JSerialCommArchFix -> device/provider/deej; MidiTransport and JavaxMidiTransport -> device/provider/midi. Completes the provider framework's cohesion (each provider owns its transport) and empties device/io. Redundant same-package imports dropped. No native-image config touched. Verified: JVM build green; coverage guards + DeejSerialProvider/MidiProvider tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AnalogBandColorService is the LED-feedback colour-override provider for CommandAnalogBands, and BandTransition is that command's advance() return type (kept out of commands/command/ to stay off the TS contract). Both move to the analogbands integration that already owns the command — AnalogBandColorService at the package root, BandTransition under command/. Verified: JVM build green; AnalogBandColorService + CommandAnalogBands tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ions Each connector now owns its persisted-config shape: DiscordSettings/DiscordAuth/ DiscordSeenUser -> integration/discord/dto, MqttSettings (+ HomeAssistantSettings) -> integration/mqtt/dto, OSCConnectionInfo/OSCBinding -> integration/osc/dto, WaveLinkSettings -> integration/wavelink/dto. Save/Profile keep the fields and import from the new homes. They go in a dto/ subpackage so the typescript-generator's **.dto.** classPattern still emits them — backend.types.ts is byte-for-byte unchanged. profiles.json is unaffected (field-name serialization, concrete types). NativeImageConfig imports + reachability-metadata (MqttSettings + $HomeAssistantSettings, OSC*, WaveLinkSettings) repointed. Verified: JVM build green; backend.types.ts unchanged; parity/coverage guards pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SndCtrlNative moved to com.getpcpanel.integration.volume.platform.windows in the audio
refactor, but the committed SndCtrl.dll still exported the old
Java_com_getpcpanel_cpp_windows_SndCtrlNative_* entry points — Windows native audio would
fail to bind at runtime. Updated the C++ accordingly:
- Renamed the JNI impl/header to com_getpcpanel_integration_volume_platform_windows_
SndCtrlNative.{cpp,h} and repointed all JNIEXPORT entry-point names.
- Updated the JNI callback method descriptors that construct the moved data classes
(...)Lcom/getpcpanel/integration/volume/platform/AudioSession; and ...AudioDevice; in
AudioSession.cpp, Listeners.h, sndctrl.cpp.
- Updated CMakeLists.txt, the VS .vcxproj/.filters, header.bat and README references.
The committed src/main/resources/SndCtrl.dll is the MSVC build produced by the new manual
"Build SndCtrl.dll (Windows)" GitHub Actions workflow (~70 KB). objdump confirms all 12
Java_com_getpcpanel_integration_volume_platform_windows_SndCtrlNative_* exports and zero
old-package exports. Behaviour is unchanged — only package-name strings differ.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ard interface The keyboard backends were public statics that every caller (KeyMacro, CommandMedia) had to platform-branch over by hand — leaking implementation choice across the codebase. Now they follow the project's own ISndCtrl idiom: - New com.getpcpanel.integration.keyboard.Keyboard interface (executeKeyStroke / typeText / sendMediaKey). Exactly one impl is in a build, chosen by @WindowsBuild/@MacBuild/@linuxbuild; callers just resolve Keyboard via CdiHelper and never check the OS. - WindowsKeyboard / OsxKeyboard / LinuxKeyboard become package-private @ApplicationScoped beans implementing Keyboard. Their pure key-mapping helpers stay package-private static (still unit-tested in-package). - Media keys are unified onto Keyboard.sendMediaKey: the Windows global-media + Spotify WM_APPCOMMAND logic moved out of CommandMedia into WindowsKeyboard; macOS delegates to the now package-private OsxMediaControl; Linux keeps its XTEST/MPRIS path. VolumeButton is now a clean cross-platform identifier (its Windows key/appcommand codes live in WindowsKeyboard). - KeyMacro is deleted (its mac-accessibility warning folded into OsxKeyboard); CommandKeystroke and CommandMedia inject Keyboard. Removed the obsolete --initialize-at-run-time=…KeyMacro from pom.xml + application.properties. CommandMedia no longer imports User32/SndCtrlWindows or branches on the OS. backend.types.ts unchanged (VolumeButton is still a name-only enum). Verified: full JVM suite 437 pass / 0 fail (CDI deploys one Keyboard bean), keyboard keystroke/native-config tests + parity/coverage guards green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…als by .class The earlier mqtt -> integration.mqtt move left String classNames in NativeImageConfig pointing at the gone com.getpcpanel.mqtt.MqttDeviceService$MqttEvent and MqttHomeAssistantHelper$* — invisible to javac, but the native build would fail with MissingReflectionRegistrationError. Fixed, and hardened against recurrence: Every PCPanel-internal class registered for reflection is now referenced by .class in the @RegisterForReflection targets block (compiler-checked) instead of by a fragile String: - NativeImageConfig: MqttDeviceService.MqttEvent, the six MqttHomeAssistantHelper.HomeAssistant* records, Version(+SemVer), and CoreAudioLib.AudioObjectPropertyAddress/ListenerProc. - JnaWin32ReflectionConfig: WinShell32(+NOTIFYICONDATA), WinUser32Ext, Win32Desktop, Win32PowerNotify. To allow this, MqttDeviceService.MqttEvent and the MqttHomeAssistantHelper discovery records were made public (the rest were already public). classNames now lists only third-party types (Eclipse Paho, jna-platform CoreFoundation/WinDef/...) whose internals we cannot reference by .class and which never move under us. Now a package move of any of our own native-registered classes is a compile error, not a silent native-build break. Verified: JVM build green; Reflection/Proxy coverage + parity guards pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…specific App-icon extraction and process enumeration feed the application/process picker used by multiple features (per-process volume, program run/focus, device assignment) — and the engine's IconService wraps IIconService directly. No program command touches them, so they are shared infrastructure, not part of the program integration: - iconextract/ (IIconService SPI + Windows/Linux/Mac impls + the JNA/COM backend) moves back to the top-level com.getpcpanel.iconextract. - IconResource (/api/icons) and ProcessResource (/api/processes) move back to com.getpcpanel.rest (the shared frontend bridge). integration/program now contains only the program command module plus IPlatformCommand — the per-OS exec/kill backend those commands actually drive (correctly program-owned). Native-image config (Shell32Extra init-at-run-time in pom + application.properties, proxy-config, and the reachability entries incl. the REST quarkusrestinvoker hashes) repointed accordingly. Verified: JVM build green; IconExtractNativeTest + parity/coverage guards pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that each feature owns a cohesive package, the implementation classes that nothing outside their package references no longer need to be public. Dropped `public` from 35 class declarations across the integration packages — platform audio backends (SndCtrlPulseAudio/SndCtrlOsx + the PulseAudio*/WindowsAudio*/OsxAudioDevice structs), the overlay impls (Win32VolumeOverlay/LinuxOverlay/NoOpOverlayWindow/OverlayDemoTrigger), the mute resolvers and icon handlers (discovered via @ALL by interface, so visibility is irrelevant to wiring), and per-integration services (MqttDeviceColorService — the example that prompted this, ObsWebSocketClient, VoiceMeeterMuteService, HomeAssistantClient, …). Selection was conservative: skipped command classes, @path REST resources, and anything referenced by .class in the native-image config (those must stay public). The compiler rejects any cross-package concrete reference, so a missed dependency is a build error, not a silent runtime break. Verified: full JVM suite 437 pass / 0 fail — CDI still deploys every bean (no Unsatisfied/Ambiguous/Unproxyable), coverage/parity guards green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The util/ root had grown to ~22 loose files. Grouped them by concern (joining the existing
version/, coloroverride/, tray/ subpackages):
- util/image/ Images, PngDecoder, PngEncoder
- util/concurrent/ Debouncer, ReconnectBackoff
- util/os/ ProcessHelper, OsxPermissionHelper, ConsoleSupport, Kernel32Console
- util/io/ FileUtil, ExtractUtil, PcPanelRoot, FileChecker
- util/app/ AppEvents, AppShutdownState, ShowMainEvent, ShowMainService, OpenFolderEvent,
StartupOnboarding
The ubiquitous cross-cutting primitives stay at the util root: CdiHelper (26 consumers),
Util, SharedHttpClient, ValueInterpolator. Native-image config repointed (the OsxPermission
Helper/Kernel32Console --initialize-at-run-time build-args in pom + application.properties,
proxy-config, and the reachability entries for AppEvents/AppShutdownState/Debouncer/
ExtractUtil/FileUtil/ProcessHelper); the moved tests follow their subjects.
Verified: full JVM suite 437 pass / 0 fail; parity/coverage guards green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cture Rewrote docs/feature-module-structure.md as the end-state document: the organising principle (integration/* = command providers only), the final package layout, and the key patterns (decentralized command registry, build-selected platform interfaces, the device HAL + providers, native-image registration by .class, the SndCtrl.dll JNI coupling), plus how to add a command / integration / provider / platform backend. Updated CLAUDE.md architecture references to match: the device HAL + PCPanel/deej/midi providers under device/, the command engine vs integration/*/command split, the OS-audio facade under integration/volume/platform, the shared-vs-feature REST split, the overlay under integration/volume, the single **.command.** classPattern, the macOS OsxKeyboard path, and the fluent-accessor lombok.config now at integration/volume/platform. (src/main/cpp C++-source references are unchanged — only the Java cpp package was dissolved.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On-demand (workflow_dispatch) job that builds the Windows native audio library with MSVC (CMake + Visual Studio generator) and uploads SndCtrl.dll as a workflow artifact. It does not attach the DLL to a release or commit it — after changing the C++ under src/main/cpp/ (e.g. when SndCtrlNative's package moves), run this, download the artifact, and commit src/main/resources/SndCtrl.dll by hand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…el provider The PCPanel-specific device implementation was sitting in the generic device HAL root, and DeviceFactory was a misleadingly-generic name for something whose build() only constructs PCPanel devices. Fixed: - PCPanelMiniDevice / PCPanelProDevice / PCPanelRGBDevice -> device/provider/pcpanel. - DescriptorFactory (the built-in PCPanel descriptor catalog) -> device/provider/pcpanel. - DeviceFactory split by ownership: the PCPanel build() becomes device/provider/pcpanel/PcPanelDeviceFactory; the generic GenericDevice construction becomes device/GenericDeviceFactory. DeviceHolder injects both and routes by provider. DeviceType stays in device/ deliberately: it is the device-kind discriminator that the generic persistence (Profile/DeviceSave/LightingConfig) and the REST/TS contract (DeviceDto) key on, so relocating it would push those generic layers to import the provider. Removing that cross-layer dependence on a PCPanel enum is the separate, in-progress device-layer generalization (docs/device-layer-generalization-plan.md), not a package move. Verified: full JVM suite 437 pass / 0 fail (CDI deploys both new factory beans); reachability metadata repointed (DeviceFactory -> PcPanelDeviceFactory + GenericDeviceFactory). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProVisualColorsService is pure PCPanel-Pro implementation — it bails unless deviceType()==PCPANEL_PRO and hard-codes the Pro geometry (5 dials, 4 sliders, 5 segments) and its rainbow/wave/custom LED rendering. It is not generic device infrastructure, so it belongs in com.getpcpanel.device.provider.pcpanel, not the HAL root. Its consumers (analogbands, mute-colour, the REST event broadcaster + device snapshot DTO) now import it from the provider — the honest representation of those features rendering Pro visuals. Verified: JVM build green; ProVisualColorsService + reflection-coverage tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts command BrightnessService is the runtime service for the brightness command (it reads CommandBrightness out of the profiles to decide the device-wide runtime brightness), so it imported integration.device.command.CommandBrightness from the device HAL — a HAL→integration smell. Moving it next to CommandBrightness in integration/device removes that inversion; the pcpanel OutputInterpreter that consumes the runtime value now depends on the brightness integration (provider→feature), which is the honest direction for a hardware-output feature. Verified: JVM build green; BrightnessServiceTest passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rovider DeviceType is the PCPanel hardware-model catalogue (PCPANEL_RGB/MINI/PRO with their USB vid/pid and control counts) — provider-internal data, not a generic HAL type. It moves to com.getpcpanel.device.provider.pcpanel. This makes the persistence (Profile/DeviceSave/Save/LightingConfig) and the REST/TS contract (DeviceDto/DeviceSnapshotDto) import the provider for the device-kind enum. That coupling is the pre-existing generalization debt — those generic layers still key on the PCPanel enum instead of the generic providerId/deviceKindId — now made explicit at the import rather than hidden by parking a PCPanel enum in the generic device root. Removing the dependency outright is the device-layer generalization (docs/device-layer-generalization-plan.md). With this, com.getpcpanel.device holds only generic HAL types (Device, GenericDevice, DeviceHolder, GenericDeviceFactory, GlobalBrightnessChangedEvent); all PCPanel-specific device code lives under device/provider/pcpanel. profiles.json and backend.types.ts are unchanged (enum name-serialized; TS emission is package-independent). Verified: full JVM suite 437 pass / 0 fail; backend.types.ts byte-identical; guards green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generator emits LF line endings, but git's `* text=auto` rewrites the checked-out command-registry.generated.ts to CRLF on Windows runners (core.autocrlf), so the freshly-rendered expected (LF) never equalled the read-from-disk actual (CRLF). Both the test and the generated file are new on this branch, so the Windows native-image build hit this for the first time and failed in the test phase. Compare content, not line endings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Feature-module (plugin-style) restructure
Restructures the application around feature modules so each feature owns its own code, instead of
the previous split where commands, REST resources, platform backends, and helpers were scattered across
generic top-level packages (
cpp/,hid/,commands/command/,rest/,util/).Guiding principle
integrationonly if it provides commands. Providers, the hardware-abstractionlayer, and infrastructure are not integrations.
volume,keyboard,program,obs,voicemeeter,wavelink,osc,mqtt,homeassistant,discord,analogbands, …) lives underintegration.<name>and ownsits
command/+CommandModule, REST, SPI impls, services, and platform backends.device/,device/provider/,device/descriptor/) is the HAL — it provides nocommands, so it is not an integration. PCPanel is one provider among Deej and MIDI.
Highlights
commands/now holds only the engine (Command, the SPIs,dispatcher,
@CommandMeta/CommandModuleregistry). Concrete commands moved into their featurepackages. Command JSON ids are location-independent (
@JsonTypeName+legacyIds), so existingsaved profiles keep working.
@CommandMeta(Java is the source of truth), guarded by
CommandRegistryGeneratorTest.interfaces (
Keyboard,ISndCtrl) — callers no longer platform-check. Many classes tightened topackage-private.
cpp/andhid/grab-bags dissolved: OS audio facade + backends →integration/volume/platform/;keyboard/media backends →
integration/keyboard/platform/; device HAL + providers →device/.SndCtrl.dllJNI exports were repointed toSndCtrlNative's newpackage and the DLL rebuilt on Windows/MSVC (a new manual
build-sndctrl-dll.ymlworkflow builds it asan artifact).
util/given structure (util/image,util/io, …); native-image config references projectinternals by
.classinstead of brittle String class names.CLAUDE.md,docs/feature-module-structure.md,docs/events.md.Validation
backend.types.tsis byte-identical to before — the frontend contract is unchanged.Flatpak (run 28329170850). A Windows-only test failure (CRLF/LF mismatch in the new
CommandRegistryGeneratorTest) was found and fixed.AI-assisted contribution disclosure
This change is AI-generated, authored by Claude (Opus 4.8) under close human direction: the human set
the goals and architecture principles, repeatedly corrected scope and design, and decided what to move
where. Code was reviewed primarily by the AI; the human reviewed direction, structure, and outcomes
rather than every line. Confidence rests heavily on the automated checks: the full JVM test suite, the
byte-identical generated TS contract, and the green native CI build on every target platform.