Skip to content

feat: view as surface - #4469

Open
Julusian wants to merge 15 commits into
mainfrom
feat/grid-view-as-v2
Open

Julusian wants to merge 15 commits into
mainfrom
feat/grid-view-as-v2

Conversation

@Julusian

@Julusian Julusian commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Related: #1352
Closes: #2189
closes #3205

Replaces #2945

This is intended to solve 2 workflow problems:

  1. When programming buttons for some surfaces, it is non obvious which grid cells align with the physical controls. The SD+XL is a good example of this with its x-xx-xx-x pattern of encoder/lcd alignment.
  2. When programming for a surface which does not have square buttons, the only way to preview the correct aspect ratio is the button style editor. It is impossible to get an overview of a whole page.

For now this is gated behind a flag in the advanced settings page, so that this can be merged before being complete (to avoid large merge conflicts)

To achieve this, there is a new button+dropdown in the button grid, which allows for choosing a surface to see the grid as. This has 2 modes, you can pick a surface that your companion currently or has previously had connected (once it has been connected recently enough for us to grab and persist the layout info), or any model reported as compatible by an enabled surface module.

image image

The grid below then gets reconfigured to match the selected surface. For modules which aren't providing a new 'surfaceAppearance' definition, we make our best attempt based upon the technical layout of the controls it requests. These are usually messy, but at least filter down the controls and provide the correct resolutions to the controls: (Some CSS polish still needed)

image image image

Surfaces can now also provide a full appearance description to drive this drawing properly. For example:

image image image

Note that these are not 100% faithful to the original, but they should be accurate enough for any view and alignment a user may want to do. Primarily as they are svgs measured from either photos or manufacturer pngs. Intentionally using svgs here to avoid the bloat of high res pngs which will need to come in over satellite.

Summary by CodeRabbit

  • New Features
    • Added “View as surface” to preview the button grid using a connected device’s layout or a declared surface model, with control shapes, colors, and LED indicators.
    • Added fit-to-panel zoom, surface selection and positioning controls, and guidance when surface controls extend beyond the configured grid.
    • Surface views now include appearance and model information.
  • Documentation
    • Updated the Buttons guide with instructions for viewing the grid as a surface.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e3c1fe1f-3228-4884-8276-a6248a385752

📥 Commits

Reviewing files that changed from the base of the PR and between 79ee3b9 and 13352bf.


⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c80cb59d-c5b6-4dc5-9071-2823149d492e

📥 Commits

Reviewing files that changed from the base of the PR and between fba3ccd and a775b9b.


⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock

📒 Files selected for processing (3)
  • companion/lib/Surface/PluginSurfaceModels.ts
  • companion/test/Surface/PluginSurfaceModels.test.ts
  • docs/user-guide/3_config/buttons/index.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.



📝 Walkthrough

Walkthrough

This change adds surface model metadata and geometry resolution, then integrates surface selection, rendering, feedback, and per-surface zoom into the Buttons view. It also adds tests and user-guide coverage.

Changes

Surface-aware Button Grid

Layer / File(s) Summary
Surface model metadata and updates
shared-lib/lib/Model/Surfaces.ts, companion/lib/Instance/Surface/*, companion/lib/Surface/*, companion/test/Surface/*, shared-lib/lib/ModuleApiVersionCheck.ts, package.json
Plugin-declared models pass through IPC, validation, controller aggregation, and model update subscriptions. Surface appearance and model identifiers are included in surface metadata and configuration.
Surface view geometry
shared-lib/lib/SurfaceLayout.ts, shared-lib/lib/SurfaceLayoutEstimate.ts, shared-lib/lib/__tests__/SurfaceLayout*.test.ts
The resolver produces face-relative control bounds, shapes, feedback, and grid placement. Face geometry is estimated when appearance data cannot provide it.
Surface and model selection
webui/src/Buttons/GridViewAs*, webui/src/Buttons/useGridViewAs.ts, webui/src/Components/Dropdown*, webui/src/Components/__tests__/DropdownInputField.test.tsx, webui/src/Buttons/__tests__/GridViewAs.test.ts, docs/user-guide/3_config/buttons/index.md
The UI stores and resolves a selected surface or model, supports model offsets, and shows status banners. Dropdown choices carry disabled state. The guide describes the surface view.
Surface canvas and grid interaction
webui/src/Buttons/SurfaceView/SurfaceCanvas*, webui/src/Buttons/SurfaceView/surfaceGeometry.ts, webui/src/Buttons/index.tsx, webui/src/Buttons/ButtonGridPanel*, webui/src/Buttons/GridViewGrowPanel.tsx, webui/src/Buttons/__tests__/ButtonGridPanel.test.tsx, webui/src/Buttons/__tests__/ButtonsPage.test.tsx
The Buttons page can render a surface canvas, navigate between its controls, and show a grid-growth action for controls outside the configured grid. The canvas supports hit testing and marquee selection.
Surface-specific zoom
webui/src/Buttons/GridZoom.tsx, webui/src/Buttons/ButtonGridZoomControl*, webui/src/Buttons/index.tsx, webui/src/Buttons/__tests__/GridZoom.test.tsx, webui/src/ImportExport/Import/Page.tsx
Surface views support fit-to-panel zoom and stored per-surface zoom levels. The zoom control displays and enables fit mode.
Color and LED feedback
shared-lib/lib/Model/Common.ts, companion/lib/Preview/Graphics.ts, webui/src/Buttons/GridButtonCell.tsx, webui/src/Buttons/GridButtonPreview.tsx, webui/src/Buttons/SurfaceView/SurfaceLeds*, webui/src/Hooks/useButtonImageForLocation.ts, webui/src/Buttons/__tests__/*, webui/src/Hooks/__tests__/useButtonImageForLocation.test.tsx, companion/test/Preview/Graphics.test.ts
Button preview data includes color and LED gauge fields. Button cells render color feedback and LED overlays in ring or strip layouts.

Priority: ⬇️ Low

Merge Risk

Merge Risk: 🔵 Low · up to a775b

The surface package pins warrant owner awareness, but no compatibility failure was established. The previously reported model-validation and guide issues are fixed; the change is mergeable with that bounded uncertainty.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a775b

Plugin-provided display metadata now reaches saved configurations and browser rendering. Validation, host-owned lifecycle management, and existing grid-location controls limit the observed exposure. No introduced vulnerability was established, but image-source restrictions and operational rollback remain incompletely verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The traced exposure begins with an initialized surface plugin and extends to host model aggregation, saved surface display metadata, and browsers displaying the selected surface. The source inspected does not establish anonymous remote access, new tenant privileges, or infrastructure authority.

Trust Boundaries and Controls

  • observed — Plugin model data crosses an IPC boundary through host validation and host-supplied instance and module identity. Module qualification and host-owned registration prevent the plugin from directly choosing an unqualified aggregate identity.
  • observed — Surface interactions map resolved controls to existing grid-location actions. Off-grid controls take the separate grow-grid path rather than rendering the normal actionable grid cell.
  • observed — The geometry resolver copies appearance.bodyImage into the resolved body image, which the renderer assigns to CSS backgroundImage. Appearance validation is delegated to the external surface base package; its image-source restrictions were not available for verification. This establishes a display sink, not a verified network-fetch or script-execution vulnerability.

Resilience and Maintainability Implications

  • observed — Instance cleanup removes transient model declarations, while disconnected physical surfaces intentionally retain saved layout metadata. The UI distinguishes missing producers and unavailable layouts, limiting silent rebinding after removal or reconnect.

Hardening Proposals

  • proposed — Confirm and document the enforced image-source policy at the shared appearance-validation boundary. If network URLs are accepted, consider restricting surface artwork to embedded or explicitly approved sources so selecting a model cannot unexpectedly initiate requests from a viewing browser. This is a proposal addressing an evidence gap, not an observed defect.



🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #2189 asks for the Buttons view to reflect an attached Stream Deck +, for rotary encoder options to be enabled by default, and for manual layout selection when no Stream Deck is attached. This P… Please add automatic selection of the attached Stream Deck + and enable its rotary encoder options by default, with tests for both behaviors. The manual selection feature already addresses the no-device selection requirement.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The surface model IPC, layout resolution, canvas rendering, zoom, dropdown support, tests, and Buttons guide all support the surface-view feature requested by issue #2189. The change summary identifie…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a surface view for the button grid.

Full details: Linked Issues check

Explanation

Issue #2189 asks for the Buttons view to reflect an attached Stream Deck +, for rotary encoder options to be enabled by default, and for manual layout selection when no Stream Deck is attached. This PR supports manual selection through GridViewAsControl and useGridViewAs. However, the new view state defaults to disabled with no selection, so the PR does not establish automatic selection of an attached Stream Deck +. The change summary also does not show implementation or tests for enabling rotary encoder options by default.



  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A face takes shape in measured space,
Controls find their positions and place.
LEDs trace a ring or strip,
Zoom fits the panel with a flip.
Models join the grid view flow,
While colors make the buttons glow.
Tests chart each change as features grow.

Comment @coderabbitai help to get the list of available commands.

@github-project-automation github-project-automation Bot moved this to In Progress in Companion Plan Sep 17, 2026
@Julusian Julusian added this to the v5.1 milestone Sep 17, 2026
@socket-security

socket-security Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​companion-surface/​host@​1.4.2 ⏵ 1.5.0-nightly-claude-keen-tesla-ekqmu3-20261003-162119-932edce78 +11007697 +1100
Updated@​companion-surface/​base@​1.4.1 ⏵ 1.5.0-nightly-claude-keen-tesla-ekqmu3-20261003-161950-932edce80 +110087 +296 +1100

View full report

@Julusian Julusian mentioned this pull request Sep 17, 2026
@peternewman

Copy link
Copy Markdown
Contributor

For modules which aren't providing a new 'surfaceAppearance' definition,
image
Surfaces can now also provide a full appearance description to drive this drawing properly. For example:

So is it just that required to do the "col span" type behaviour on the Neo to get the "long" button at the bottom to align with the four normal buttons above it, or is there some additional work/future enhancement required first?

I assume the pain of stuff like the Shuttle with a row of four buttons, then a row of five below will always be a pain/you won't necessarily support different column spacing (I guess like the SD + XL), although in theory with some clever col spans, it ought to be possible to represent.

@Julusian

Copy link
Copy Markdown
Member Author

The current screenshots show the basic 'auto' mode of displaying a surface. Our best guess at how to arrange things with the limited information provided.

The part I am still working on is detailed drawing, relying on the surface module providing more details. This is currently achieving the below but it needs some more work (Largely background images, and figuring out some edge cases):
image
image

@peternewman

Copy link
Copy Markdown
Contributor

The part I am still working on is detailed drawing, relying on the surface module providing more details. This is currently achieving the below but it needs some more work

That already looks pretty awesome for those ones.

(Largely background images, and figuring out some edge cases):

Are those edge cases likely to go as far as the complexities of Shuttle which is effectively a doughnut and then the whole as separate controls in terms of the jog shuttle?

@Julusian
Julusian force-pushed the feat/grid-view-as-v2 branch 4 times, most recently from d8a91b5 to abf4777 Compare October 3, 2026 17:54
@Julusian
Julusian force-pushed the feat/grid-view-as-v2 branch 2 times, most recently from e2e8c4a to fba3ccd Compare October 3, 2026 21:06
@Julusian
Julusian marked this pull request as ready for review October 3, 2026 21:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dd7612af-9b83-4b27-afc6-14d7cb665afb
📥 Commits

Reviewing files that changed from the base of the PR and between 5eaea25 and fba3ccd.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (66)
  • companion/lib/Instance/Surface/ChildHandler.ts
  • companion/lib/Instance/Surface/IpcTypes.ts
  • companion/lib/Instance/Surface/Thread/Entrypoint.ts
  • companion/lib/Preview/Graphics.ts
  • companion/lib/Surface/Config.ts
  • companion/lib/Surface/Controller.ts
  • companion/lib/Surface/Handler.ts
  • companion/lib/Surface/IP/ElgatoEmulator.ts
  • companion/lib/Surface/IP/Satellite.ts
  • companion/lib/Surface/LayoutSummary.ts
  • companion/lib/Surface/PluginPanel.ts
  • companion/lib/Surface/PluginSurfaceModels.ts
  • companion/lib/Surface/Types.ts
  • companion/test/Preview/Graphics.test.ts
  • companion/test/Surface/ControllerLayouts.test.ts
  • companion/test/Surface/ControllerModels.test.ts
  • companion/test/Surface/LayoutSummary.test.ts
  • companion/test/Surface/PluginSurfaceModels.test.ts
  • docs/user-guide/3_config/buttons/index.md
  • package.json
  • shared-lib/lib/Model/Common.ts
  • shared-lib/lib/Model/Surfaces.ts
  • shared-lib/lib/ModuleApiVersionCheck.ts
  • shared-lib/lib/SurfaceLayout.ts
  • shared-lib/lib/SurfaceLayoutEstimate.ts
  • shared-lib/lib/__tests__/SurfaceLayout.test.ts
  • shared-lib/lib/__tests__/SurfaceLayoutEstimate.test.ts
  • webui/src/Buttons/ButtonGridPanel.css
  • webui/src/Buttons/ButtonGridPanel.tsx
  • webui/src/Buttons/ButtonGridZoomControl.stories.tsx
  • webui/src/Buttons/ButtonGridZoomControl.tsx
  • webui/src/Buttons/ButtonInfiniteGrid.tsx
  • webui/src/Buttons/GridButtonCell.tsx
  • webui/src/Buttons/GridButtonPreview.tsx
  • webui/src/Buttons/GridViewAs.css
  • webui/src/Buttons/GridViewAs.ts
  • webui/src/Buttons/GridViewAsBanner.stories.tsx
  • webui/src/Buttons/GridViewAsBanner.tsx
  • webui/src/Buttons/GridViewAsControl.tsx
  • webui/src/Buttons/GridViewGrowPanel.tsx
  • webui/src/Buttons/GridZoom.tsx
  • webui/src/Buttons/SurfaceView/SurfaceCanvas.css
  • webui/src/Buttons/SurfaceView/SurfaceCanvas.tsx
  • webui/src/Buttons/SurfaceView/SurfaceLeds.css
  • webui/src/Buttons/SurfaceView/SurfaceLeds.tsx
  • webui/src/Buttons/SurfaceView/__tests__/ledLayout.test.ts
  • webui/src/Buttons/SurfaceView/__tests__/surfaceGeometry.test.ts
  • webui/src/Buttons/SurfaceView/ledLayout.ts
  • webui/src/Buttons/SurfaceView/surfaceGeometry.ts
  • webui/src/Buttons/__tests__/ButtonGridPanel.test.tsx
  • webui/src/Buttons/__tests__/ButtonsPage.test.tsx
  • webui/src/Buttons/__tests__/GridButtonDragOverlay.test.tsx
  • webui/src/Buttons/__tests__/GridButtonIcons.test.tsx
  • webui/src/Buttons/__tests__/GridButtonPreview.test.tsx
  • webui/src/Buttons/__tests__/GridViewAs.test.ts
  • webui/src/Buttons/__tests__/GridZoom.test.tsx
  • webui/src/Buttons/index.tsx
  • webui/src/Buttons/useGridViewAs.ts
  • webui/src/Components/DropdownChoices.tsx
  • webui/src/Components/DropdownInputField/Popup.tsx
  • webui/src/Components/DropdownInputField/useFuzzyChoices.ts
  • webui/src/Components/DropdownInputFieldSimple.tsx
  • webui/src/Components/__tests__/DropdownInputField.test.tsx
  • webui/src/Hooks/__tests__/useButtonImageForLocation.test.tsx
  • webui/src/Hooks/useButtonImageForLocation.ts
  • webui/src/ImportExport/Import/Page.tsx

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread companion/lib/Surface/PluginSurfaceModels.ts
Comment thread docs/user-guide/3_config/buttons/index.md Outdated
Comment thread package.json
@Julusian

Julusian commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

I am happy with this now, and think it is ready to merge and update other modules to properly support.

@peternewman check out the last 3 screenshots in the description. Thats what I've been aiming for, and will hopefully translate to other surface modules just fine.
The earlier screenshots still apply for when these detailed layouts havent been supplied

@Julusian
Julusian force-pushed the feat/grid-view-as-v2 branch from fba3ccd to a775b9b Compare October 3, 2026 22:01
@krocheck

krocheck commented Oct 3, 2026

Copy link
Copy Markdown
Member

This looks great; nice work!

@Julusian
Julusian added this pull request to stack #4497 October 3, 2026 22:28
@Julusian Julusian modified the milestones: v5.1, v5.2 Oct 6, 2026
@Julusian
Julusian force-pushed the feat/grid-view-as-v2 branch from a775b9b to 79ee3b9 Compare October 8, 2026 21:07
@Julusian
Julusian removed this pull request from stack #4497 October 8, 2026 21:12
@Julusian
Julusian added this pull request to stack #4505 October 8, 2026 21:12
Julusian and others added 2 commits October 9, 2026 18:50
A choice can say it is disabled, and both the simple and the searchable
dropdown then show it without letting it be chosen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Julusian and others added 13 commits October 9, 2026 18:50
The surface api for declaring the models a plugin supports, describing the
appearance of a surface, and saying which declared model a surface is, is not
yet in a release.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The grid is larger than any one surface, and a surface is not always a plain
rectangle of square buttons. Viewing the grid as a surface - one which has
been connected, or a model of one a surface module declares - draws the grid
as the face of that surface: each control where it sits and at the shape it
is drawn, with the cell coordinates still the grid's own. Controls beyond the
configured grid are drawn but cannot be configured, and offer to grow the
grid to fit.

The choice is made per browser, as a way of looking at the grid rather than
part of the config.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A surface can describe its face: where each control sits and its shape, the
colour of its body, and artwork for it. That is kept with the surface's config
alongside its layout, so it is known while the surface is unplugged, and the
view draws it exactly rather than estimating positions from the layout's rows
and columns. Artwork is drawn without the body colour under it, so the shape
of the device shows, and a control is only rounded when given a radius.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in ones

A fixed few pixels taken off every side of a control was nothing on a key but
squashed a thin one, like the Neo's info bar, out of the shape its image is
drawn at. Controls are now drawn exactly where the surface has them, as the
face already leaves gaps between them.

A control thinner than a fingertip is given a hit area grown around it, cut
back to the middle of the gap wherever it would meet another's, so even a
dense face never has two overlap. One too small to hold its location leaves it
out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A style with colours and no bitmap, like the Neo's touch buttons, is lit
rather than drawn on. Such a control is drawn as a swatch of the button's
background, which the preview stream now carries, rather than its image
squeezed into a sliver it was never going to be shown in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A control's style can declare leds, as a ring around an encoder or a strip.
The preview stream now carries the gauge a button drives them with, and the
view draws them around or along the control, each segment lit with the same
sampling the surface is driven with.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The dark ground the grid uses fights with a face of any colour but its own, so
a face the surface describes is drawn on the page instead, with a hairline and
soft shadow tracing its outline, and scrollbars to match. Press mode keeps the
armed ground, as marking out what is live matters more there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The panel's min-height was only ever set by the grid, so a surface face kept
whatever the grid had last asked for, and zooming out left the panel far
taller than the face. The face now asks for its own: the whole face when it is
short, or two rows of keys when it is tall, so it scrolls within the panel
rather than stretching the page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…by default

A Neo and a Studio want very different zoom levels, so one shared between them
is always wrong for one. Viewing as a surface has a zoom of its own, remembered
for its model so a plugged in surface shares it with its model. One not zoomed
by hand fits to the panel's width, following it as it changes, within 60-100%
so a very wide surface stays readable and a small one is not blown up. The zoom
control offers going back to fitting.

Steps work from the previous setting, so a burst of them - a fast ctrl+wheel -
all count, and the controller only changes with the kind of surface, so the
wheel listener holding it is not redone at every step.

The grid's own zoom now starts at 100% when storage cannot be read, rather
than failing the buttons page where storage is blocked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A surface can say which of its module's declared models it is. That is kept
with its config, like its appearance, so it is still known while the surface
is unplugged, and the layout summary qualifies it with the module, as the
declared models are.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Matching by the model id a surface reports, before falling back to its name,
which a module is free to word differently from the model's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A display bigger than the others in its row or column, like an info bar under
a row of keys, now spills into the empty cells beside it rather than widening
its own track, so the keys around it stay evenly spaced. A cell two would both
spill into is left to neither.

A control with no display fills the cell the displays around it leave, rather
than being drawn a fixed square, while one with a ring of leds stays round.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Julusian
Julusian force-pushed the feat/grid-view-as-v2 branch from 79ee3b9 to 13352bf Compare October 9, 2026 17:50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

request: ButtonGrid view quick change on GUI button click Button view for Stream Deck +

3 participants