diff --git a/DemoPlugin/.gitignore b/DemoPlugin/.gitignore
index 783bc57..74ae05a 100644
--- a/DemoPlugin/.gitignore
+++ b/DemoPlugin/.gitignore
@@ -1,14 +1,21 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
+*.rsuser
*.suo
+*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
+# Mono auto generated files
+mono_crash.*
+
# Build results
[Dd]ebug/
[Dd]ebugPublic/
@@ -16,42 +23,62 @@
[Rr]eleases/
x64/
x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
+[Ll]ogs/
-# Visual Studio 2015 cache/options directory
+# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
-# NUNIT
+# NUnit
*.VisualState.xml
TestResult.xml
+nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
-# DNX
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
*_i.c
*_p.c
-*_i.h
+*_h.h
*.ilk
*.meta
*.obj
+*.iobj
*.pch
*.pdb
+*.ipdb
*.pgc
*.pgd
*.rsp
@@ -61,7 +88,9 @@ artifacts/
*.tlh
*.tmp
*.tmp_proj
+*_wpftmp.csproj
*.log
+*.tlog
*.vspscc
*.vssscc
.builds
@@ -89,6 +118,9 @@ ipch/
*.vspx
*.sap
+# Visual Studio Trace Files
+*.e2e
+
# TFS 2012 Local Workspace
$tf/
@@ -100,15 +132,25 @@ _ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
-# JustCode is a .NET coding add-in
-.JustCode
-
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
# NCrunch
_NCrunch_*
.*crunch*.local.xml
@@ -140,9 +182,9 @@ publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
-# TODO: Comment the next line if you want to checkin your web deploy settings
+# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
-#*.pubxml
+*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
@@ -152,13 +194,15 @@ PublishScripts/
# NuGet Packages
*.nupkg
+# NuGet Symbol Packages
+*.snupkg
# The packages folder can be ignored because of Package Restore
-**/packages/*
+**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
-!**/packages/build/
+!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
-#!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignoreable files
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
@@ -175,12 +219,15 @@ AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
-!*.[Cc]ache/
+!?*.[Cc]ache/
# Others
ClientBin/
@@ -191,9 +238,12 @@ ClientBin/
*.jfm
*.pfx
*.publishsettings
-node_modules/
orleans.codegen.cs
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
@@ -208,15 +258,22 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
+*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
@@ -226,6 +283,7 @@ FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
+node_modules/
# Visual Studio 6 build log
*.plg
@@ -233,6 +291,20 @@ FakesAssemblies/
# Visual Studio 6 workspace options file
*.opt
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
@@ -248,63 +320,81 @@ paket-files/
# FAKE - F# Make
.fake/
-# JetBrains Rider
-.idea/
-*.sln.iml
-
-# CodeRush
-.cr/
+# CodeRush personal settings
+.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
-#
-# ANGULAR gitignore begin section
-#
+# OpenCover UI analysis results
+OpenCover/
-# See http://help.github.com/ignore-files/ for more about ignoring files.
+# Azure Stream Analytics local run output
+ASALocalRun/
-# compiled output
-/dist
-/tmp
-/out-tsc
+# MSBuild Binary and Structured Log
+*.binlog
-# dependencies
-/node_modules
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
-# IDEs and editors
-/.idea
-.project
-.classpath
-.c9/
-*.launch
-.settings/
-*.sublime-workspace
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
-# IDE - VSCode
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
+*.code-workspace
-# misc
-/.sass-cache
-/connect.lock
-/coverage
-/libpeerconnection.log
-npm-debug.log
-testem.log
-/typings
+# Local History for Visual Studio Code
+.history/
-# e2e
-/e2e/*.js
-/e2e/*.map
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
-# System Files
-*.xcuserstate
-*.xcactivitylog
+# JetBrains Rider
+*.sln.iml
-*.DotSettings
+.DS_Store
\ No newline at end of file
diff --git a/DemoPlugin/.vscode/launch.json b/DemoPlugin/.vscode/launch.json
new file mode 100644
index 0000000..f67e0ab
--- /dev/null
+++ b/DemoPlugin/.vscode/launch.json
@@ -0,0 +1,17 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Attach to Logi Plugin Service",
+ "type": "coreclr",
+ "request": "attach",
+ "windows": {
+ "processName": "LogiPluginService.exe"
+ },
+ "osx": {
+ "processName": "LogiPluginService"
+ },
+ "justMyCode": true
+ }
+ ]
+}
diff --git a/DemoPlugin/.vscode/tasks.json b/DemoPlugin/.vscode/tasks.json
new file mode 100644
index 0000000..9d9ae1b
--- /dev/null
+++ b/DemoPlugin/.vscode/tasks.json
@@ -0,0 +1,40 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build (Debug)",
+ "type": "shell",
+ "command": "dotnet build -c Debug",
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Build (Release)",
+ "type": "shell",
+ "command": "dotnet build -c Release",
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Package Plugin",
+ "type": "shell",
+ "command": "logiplugintool pack ./bin/Release ./Demo.lplug4",
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Install Plugin",
+ "type": "shell",
+ "command": "logiplugintool install ./Demo.lplug4",
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Uninstall Plugin",
+ "type": "shell",
+ "command": "logiplugintool uninstall Demo",
+ "group": "none",
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/DemoPlugin/DemoPlugin.sln b/DemoPlugin/DemoPlugin.sln
index 6bd8cd4..c782f2e 100644
--- a/DemoPlugin/DemoPlugin.sln
+++ b/DemoPlugin/DemoPlugin.sln
@@ -3,7 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34408.163
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoPlugin", "DemoPlugin\DemoPlugin.csproj", "{58C04AC4-DFCD-49F9-9BF3-E8240592F3BF}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoPlugin", "src\DemoPlugin.csproj", "{A4AA1C9B-F4DA-4B58-A5ED-A95E06D70199}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD08438B-1C4A-4FFA-A856-D5285D570BB8}"
+ ProjectSection(SolutionItems) = preProject
+ .vscode\launch.json = .vscode\launch.json
+ .vscode\tasks.json = .vscode\tasks.json
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,15 +17,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {58C04AC4-DFCD-49F9-9BF3-E8240592F3BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {58C04AC4-DFCD-49F9-9BF3-E8240592F3BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {58C04AC4-DFCD-49F9-9BF3-E8240592F3BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {58C04AC4-DFCD-49F9-9BF3-E8240592F3BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A4AA1C9B-F4DA-4B58-A5ED-A95E06D70199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A4AA1C9B-F4DA-4B58-A5ED-A95E06D70199}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A4AA1C9B-F4DA-4B58-A5ED-A95E06D70199}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A4AA1C9B-F4DA-4B58-A5ED-A95E06D70199}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {F627919D-59F2-470B-BFAB-8D932B3514F8}
+ SolutionGuid = {1C04A10A-A4D3-4205-94BE-47CB0006A47F}
EndGlobalSection
EndGlobal
diff --git a/DemoPlugin/DemoPlugin/ButtonSwitchesCommand.cs b/DemoPlugin/DemoPlugin/ButtonSwitchesCommand.cs
deleted file mode 100644
index cfc1f7b..0000000
--- a/DemoPlugin/DemoPlugin/ButtonSwitchesCommand.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-namespace Loupedeck.DemoPlugin
-{
- using System;
-
- public class ButtonSwitchesCommand : PluginDynamicCommand
- {
- private const int NumberOfSwitches = 4;
- private readonly Boolean[] _switches = new Boolean[NumberOfSwitches];
-
- public ButtonSwitchesCommand() : base()
- {
- for (var i = 0; i < NumberOfSwitches; i++)
- {
- // Parameter is the switch index
- var actionParameter = i.ToString();
-
- // Add parameter
- this.AddParameter(actionParameter, $"Switch {i}", "Switches");
- }
- }
-
- protected override void RunCommand(String actionParameter)
- {
- if (Int32.TryParse(actionParameter, out var i))
- {
- // Turn the switch
- this._switches[i] = !this._switches[i];
-
- // Inform Loupedeck that command display name and/or image has changed
- this.ActionImageChanged(actionParameter);
- }
- }
-
- protected override String GetCommandDisplayName(String actionParameter, PluginImageSize imageSize)
- {
- if (Int32.TryParse(actionParameter, out var i))
- {
- return $"Switch {i}: {this._switches[i]}";
- }
- else
- {
- return null;
- }
- }
- }
-}
diff --git a/DemoPlugin/DemoPlugin/CounterAdjustment.cs b/DemoPlugin/DemoPlugin/CounterAdjustment.cs
deleted file mode 100644
index 20fbd93..0000000
--- a/DemoPlugin/DemoPlugin/CounterAdjustment.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-namespace Loupedeck.DemoPlugin
-{
- using System;
-
- // This class implements an example adjustment that counts the rotation ticks of a dial.
-
- public class CounterAdjustment : PluginDynamicAdjustment
- {
- // This variable holds the current value of the counter.
- private Int32 _counter = 0;
-
- // Initializes the adjustment class.
- // When `hasReset` is set to true, a reset command is automatically created for this adjustment.
- public CounterAdjustment()
- : base(displayName: "Counter", description: "Counts rotation ticks", groupName: "Adjustments", hasReset: true)
- {
- }
-
- // This method is called when the dial associated to the plugin is rotated.
- protected override void ApplyAdjustment(String actionParameter, Int32 diff)
- {
- this._counter += diff; // Increase or decrease the counter by the number of ticks.
- this.AdjustmentValueChanged(); // Notify the Loupedeck service that the adjustment value has changed.
- PluginLog.Info($"Counter value was changed by {diff} ticks");
- }
-
- // This method is called when the reset command related to the adjustment is executed.
- protected override void RunCommand(String actionParameter)
- {
- this._counter = 0; // Reset the counter.
- this.AdjustmentValueChanged(); // Notify the Loupedeck service that the adjustment value has changed.
- PluginLog.Info("Counter was reset");
- }
-
- // Returns the adjustment value that is shown next to the dial.
- protected override String GetAdjustmentValue(String actionParameter) => this._counter.ToString();
- }
-}
diff --git a/DemoPlugin/DemoPlugin/DemoPlugin.csproj.user b/DemoPlugin/DemoPlugin/DemoPlugin.csproj.user
deleted file mode 100644
index c997f50..0000000
--- a/DemoPlugin/DemoPlugin/DemoPlugin.csproj.user
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- Program
- C:\Program Files\Logi\LogiPluginService\LogiPluginService.exe
-
-
- Program
- /Applications/Utilities/LogiPluginService.app
-
-
\ No newline at end of file
diff --git a/DemoPlugin/DemoPlugin/ThumbUpDownCommand.cs b/DemoPlugin/DemoPlugin/ThumbUpDownCommand.cs
deleted file mode 100644
index 2aea404..0000000
--- a/DemoPlugin/DemoPlugin/ThumbUpDownCommand.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace Loupedeck.DemoPlugin
-{
- using System;
-
- public class ThumbUpDownCommand : PluginDynamicCommand
- {
- private Boolean _isThumbDown = false;
-
- private readonly String _imageResourcePathThumbUp;
- private readonly String _imageResourcePathThumbDown;
-
- public ThumbUpDownCommand() : base(displayName: "Thumb up/down", description: null, groupName: "Switches")
- {
- this._imageResourcePathThumbUp = PluginResources.FindFile("ThumbUp.png");
- this._imageResourcePathThumbDown = PluginResources.FindFile("ThumbDown.png");
- }
-
- protected override void RunCommand(String actionParameter)
- {
- this._isThumbDown = !this._isThumbDown;
- this.ActionImageChanged();
- }
-
- protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize)
- {
- var resourcePath = this._isThumbDown ? this._imageResourcePathThumbDown : this._imageResourcePathThumbUp;
- return PluginResources.ReadImage(resourcePath);
- }
- }
-}
diff --git a/DemoPlugin/DemoPlugin/ToggleMuteCommand.cs b/DemoPlugin/DemoPlugin/ToggleMuteCommand.cs
deleted file mode 100644
index a96e858..0000000
--- a/DemoPlugin/DemoPlugin/ToggleMuteCommand.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Loupedeck.DemoPlugin
-{
- using System;
-
- public class ToggleMuteCommand : PluginDynamicCommand
- {
- public ToggleMuteCommand()
- : base(displayName: "Toggle Mute", description: "Mutes and unmutes system volume", groupName: "Audio")
- {
- }
-
- protected override void RunCommand(String actionParameter)
- {
- this.Plugin.ClientApplication.SendKeyboardShortcut(VirtualKeyCode.VolumeMute);
- }
- }
-}
diff --git a/DemoPlugin/DemoPlugin/package/icontemplates/Loupedeck.DemoPlugin.ToggleMuteCommand.ict b/DemoPlugin/DemoPlugin/package/icontemplates/Loupedeck.DemoPlugin.ToggleMuteCommand.ict
deleted file mode 100644
index 0a43af6..0000000
--- a/DemoPlugin/DemoPlugin/package/icontemplates/Loupedeck.DemoPlugin.ToggleMuteCommand.ict
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "backgroundColor": 4278190080,
- "items": [
- {
- "$type": "Loupedeck.Service.ActionIconImageItem, LoupedeckShared",
- "image": "iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAABwwAAAcMBOnufsAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAe5JREFUeF7t2rFNA0EQhWEHFEIhBJRCCZRARoioAHLojgKOHYmVVqvf5glm70bWG+lL/l07mMA+LE7bttk/YDQdRtNhNB1G02E0HUbTYTQdRtNhNB1G02E0HUbTYTQdRtNhNB1G02E0HUbTYTQdRtNhNB1G02E0HUbTYTQdxkLeoZWCsYinJobOysBYwEPTh87LwHiwcXkxdKcMjAealxdD98rAeBBaXgzdLQPjAc4tL4bul4FxZ5eWF0OvKQNjstvm/ozH5reh97xrnqc2irO4Q2epMCbrz3N/nfn9YjFfTcxLM59Hi4k7y5eIMVn2At+accYl9uX1ibvja9NhTJa9wJvmoxknFjcvL+7E3fn1qTAmy15goCWOs8vyAsZkKxYYzi1xt+UFjMlWLTC8NvNEo7tLYEy2aoHzZ9444xfLUhiTrVjgvLzPH+PsskSMybIXGA/J4/TPPPpMvPSwnQJjsuwFjg/S8xfGuMSreZBe9adcPCTTt220OFu+vIBxZ/4xIYF/zkrgH1QT0BLpXhkYDzYvke6UgbGAcYl0XgbGIvrzI52VgbEQ/2vHtcNoOoymw2g6jKbDaDqMpsNoOoymw2g6jKbDaDqMpsNoOoymw2g6jKbDaDqMpsNoOoymw2g6jKbDaDqMpsNoqu30DbZ472o00s59AAAAAElFTkSuQmCC",
- "imageFileName": "ToggleMute.png",
- "imageColor": 4294967295,
- "imageRotation": "None",
- "isVisible": true,
- "itemType": "Image",
- "area": {
- "x": 0,
- "y": -10,
- "width": 100,
- "height": 100,
- "isFullScreen": true
- }
- },
- {
- "$type": "Loupedeck.Service.ActionIconTextItem, LoupedeckShared",
- "text": "Toggle Mute",
- "textColor": 4294967295,
- "fontSize": 5,
- "fontName": "Brown Logitech Pan Light",
- "isVisible": true,
- "itemType": "Text",
- "area": {
- "x": 0,
- "y": 70,
- "width": 100,
- "height": 16,
- "isFullScreen": false
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/DemoPlugin/DemoPlugin/package/metadata/DefaultIconTemplate.ict b/DemoPlugin/DemoPlugin/package/metadata/DefaultIconTemplate.ict
deleted file mode 100644
index cb606e1..0000000
--- a/DemoPlugin/DemoPlugin/package/metadata/DefaultIconTemplate.ict
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "backgroundColor": 4278869247,
- "items": [
- {
- "image": "",
- "imageFileName": null,
- "imageColor": 4294967295,
- "imageRotation": "None",
- "isVisible": true,
- "itemType": "Image",
- "area": {
- "x": 15,
- "y": 0,
- "width": 70,
- "height": 70
- }
- },
- {
- "text": "",
- "textColor": 4294967295,
- "fontSize": 5,
- "fontName": "Brown Logitech Pan Light",
- "isVisible": true,
- "itemType": "Text",
- "area": {
- "x": 0,
- "y": 70,
- "width": 100,
- "height": 30
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/DemoPlugin/DemoPlugin/package/metadata/LoupedeckPackage.yaml b/DemoPlugin/DemoPlugin/package/metadata/LoupedeckPackage.yaml
deleted file mode 100644
index 36bfc9a..0000000
--- a/DemoPlugin/DemoPlugin/package/metadata/LoupedeckPackage.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-# ==================================================================================================
-# GENERAL SETTINGS
-# ==================================================================================================
-
-# Package type. Must be plugin4 for plugins.
-type: plugin4
-
-# Name that identifies the plugin.
-name: Demo
-
-# Plugin name that is shown to the user.
-displayName: Demo
-
-# Short description of the plugin.
-description: This is a demo plugin
-
-# Name of the plugin library file.
-pluginFileName: DemoPlugin.dll
-
-# Plugin version.
-version: 1.0
-
-# Author of the plugin. The author can be a company or an individual developer.
-author: Logitech Europe SA
-
-# Copyright information.
-copyright: Copyright © 2025 Logitech Europe SA. All rights reserved.
-
-# Minimum SDK version supported by the plugin.
-minimumLoupedeckVersion: 6.0
-
-
-# ==================================================================================================
-# PLUGIN PROPERTIES
-# ==================================================================================================
-
-# Location of plugin files on Windows (relative to the plugin base directory).
-# This parameter is required to support Windows.
-pluginFolderWin: bin
-
-# Location of plugin files on macOS (relative to the plugin base directory).
-# This parameter is required to support Mac.
-pluginFolderMac: bin
-
-# List of devices the plugin is optimized for.
-supportedDevices:
- # LoupedeckCtFamily covers the following devices: Loupedeck CT, Live and Live S,
- # Razer Stream Controller, and Razer Stream Controller X.
- - LoupedeckCtFamily
-
- # Logitech MX Creative Console family devices.
- - LoupedeckExtendedFamily
-
diff --git a/DemoPlugin/README.md b/DemoPlugin/README.md
new file mode 100644
index 0000000..1181174
--- /dev/null
+++ b/DemoPlugin/README.md
@@ -0,0 +1,73 @@
+# Welcome to Logi Actions SDK
+
+Here you can find an introduction to Logi Actions SDK, example plugin source code and the SDK documentation.
+
+## Documentation
+
+- [Logi Actions SDK Developer Docs](https://logitech.github.io/actions-sdk-docs/)
+- [Logi Developer Discord](https://discord.gg/ptV2BfHCmm)
+
+---
+
+# DemoPlugin
+
+A Logitech Actions SDK plugin built with C# (.NET 8) that works on both macOS and Windows. This project is intended as a learning resource for new developers joining the team — each file is structured and commented to help you understand how the SDK works.
+
+## Folder Structure
+
+```
+DemoPlugin/
+├── .vscode/ # VS Code setup — debugger config to attach to the Logi Plugin Service
+│ # and build tasks for quick builds, packaging and installing
+├── src/
+│ ├── Actions/ # Where all commands and adjustments live.
+│ # Each file is one feature assigned to a button or dial
+│ ├── Helpers/ # Shared utility classes used across all actions.
+│ # PluginLog for logging, PluginResources for loading images
+│ ├── images/ # PNG icons embedded into the plugin DLL.
+│ # These are displayed on device buttons at runtime
+│ └── package/ # Plugin identity files read by the Logi Plugin Service
+│ # on startup — name, version, icon and supported devices
+└── bin/ # Auto-generated build output. Never edit this folder manually.
+```
+
+## Actions
+
+| Action | Type | Description |
+|---|---|---|
+| Toggle Mute | Button | Mutes and unmutes system volume on macOS and Windows |
+| Button Switches | Button | 4 independent on/off toggle switches |
+| Thumb Up/Down | Button | Toggles between thumb up and thumb down image |
+| Counter | Dial | Counts rotation ticks, press dial to reset |
+
+## Build
+
+```bash
+# Debug — used during development and debugging
+dotnet build -c Debug
+
+# Release — used for packaging and distribution
+dotnet build -c Release
+```
+
+## Debug
+
+1. Open the project in VS Code
+2. Run `dotnet build -c Debug`
+3. Go to **Run > Start Debugging** (or press `F5`)
+4. Select **Attach to Logi Plugin Service**
+
+You can now set breakpoints in any action file and step through your code live.
+
+## Package & Install
+
+```bash
+# Package the plugin into a distributable .lplug4 file
+logiplugintool pack ./bin/Release ./Demo.lplug4
+
+# Install the plugin on this machine
+logiplugintool install ./Demo.lplug4
+
+# Uninstall the plugin
+logiplugintool uninstall Demo
+```
diff --git a/DemoPlugin/.editorconfig b/DemoPlugin/src/.editorconfig
similarity index 86%
rename from DemoPlugin/.editorconfig
rename to DemoPlugin/src/.editorconfig
index cc8b97a..8ec3771 100644
--- a/DemoPlugin/.editorconfig
+++ b/DemoPlugin/src/.editorconfig
@@ -199,8 +199,44 @@ dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_diagnostic.CA1028.severity = none
dotnet_diagnostic.CA1031.severity = none
-dotnet_diagnostic.CA1303.severity = none
+dotnet_diagnostic.CA1054.severity = none
+dotnet_diagnostic.CA1056.severity = none
dotnet_diagnostic.CA1060.severity = none
+dotnet_diagnostic.CA1303.severity = none
+dotnet_diagnostic.CA1308.severity = none
+dotnet_diagnostic.CA1716.severity = none
+dotnet_diagnostic.CA1720.severity = none
dotnet_diagnostic.CA2101.severity = none
+dotnet_diagnostic.CA2234.severity = none
+dotnet_diagnostic.CA5350.severity = none
+dotnet_diagnostic.CA9998.severity = none
+
+dotnet_diagnostic.CS1591.severity = none
+dotnet_diagnostic.IDE0002.severity = none
+dotnet_diagnostic.IDE0021.severity = warning
+dotnet_diagnostic.IDE0022.severity = warning
dotnet_diagnostic.IDE0058.severity = none
+dotnet_diagnostic.IDE0059.severity = none
+
+dotnet_diagnostic.SA1108.severity = none
+dotnet_diagnostic.SA1117.severity = none
+dotnet_diagnostic.SA1121.severity = none
+dotnet_diagnostic.SA1122.severity = none
+dotnet_diagnostic.SA1131.severity = none
+dotnet_diagnostic.SA1201.severity = none
+dotnet_diagnostic.SA1202.severity = none
+dotnet_diagnostic.SA1203.severity = none
+dotnet_diagnostic.SA1204.severity = none
+dotnet_diagnostic.SA1214.severity = none
+dotnet_diagnostic.SA1309.severity = none
+dotnet_diagnostic.SA1512.severity = none
+dotnet_diagnostic.SA1513.severity = none
+dotnet_diagnostic.SA1600.severity = none
+dotnet_diagnostic.SA1601.severity = none
+dotnet_diagnostic.SA1602.severity = none
+dotnet_diagnostic.SA1629.severity = none
+dotnet_diagnostic.SA1633.severity = none
+
+dotnet_diagnostic.SX1309.severity = warning
+dotnet_diagnostic.SX1309S.severity = warning
diff --git a/DemoPlugin/src/Actions/ButtonSwitchesCommand.cs b/DemoPlugin/src/Actions/ButtonSwitchesCommand.cs
new file mode 100644
index 0000000..e715a8c
--- /dev/null
+++ b/DemoPlugin/src/Actions/ButtonSwitchesCommand.cs
@@ -0,0 +1,54 @@
+namespace Loupedeck.DemoPlugin
+{
+ using System;
+
+ // This command creates multiple toggle switches assigned to buttons.
+ // Each switch independently tracks its own on/off state.
+
+ public class ButtonSwitchesCommand : PluginDynamicCommand
+ {
+ // Total number of switches available.
+ private const Int32 NumberOfSwitches = 4;
+
+ // Array to store the on/off state of each switch.
+ private readonly Boolean[] _switches = new Boolean[NumberOfSwitches];
+
+ // Initializes the command and adds a parameter for each switch.
+ public ButtonSwitchesCommand() : base()
+ {
+ for (var i = 0; i < NumberOfSwitches; i++)
+ {
+ // Each switch uses its index as the action parameter.
+ var actionParameter = i.ToString();
+
+ // Register each switch as a separate button in the "Switches" group.
+ this.AddParameter(actionParameter, $"Switch {i}", "Switches");
+ }
+ }
+
+ // This method is called when the user presses a switch button.
+ protected override void RunCommand(String actionParameter)
+ {
+ if (Int32.TryParse(actionParameter, out var i))
+ {
+ // Toggle the switch state.
+ this._switches[i] = !this._switches[i];
+
+ // Notify the Plugin Service to refresh the button label.
+ this.ActionImageChanged(actionParameter);
+ }
+ }
+
+ // Returns the button label showing the switch index and its current state.
+ protected override String GetCommandDisplayName(String actionParameter, PluginImageSize imageSize)
+ {
+ if (Int32.TryParse(actionParameter, out var i))
+ {
+ // Show "Switch 0: On" or "Switch 0: Off" based on state.
+ return $"Switch {i}: {(this._switches[i] ? "On" : "Off")}";
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/DemoPlugin/src/Actions/CounterAdjustment.cs b/DemoPlugin/src/Actions/CounterAdjustment.cs
new file mode 100644
index 0000000..f36b9c5
--- /dev/null
+++ b/DemoPlugin/src/Actions/CounterAdjustment.cs
@@ -0,0 +1,49 @@
+namespace Loupedeck.DemoPlugin
+{
+ using System;
+
+ // This adjustment counts the rotation ticks of a dial/encoder.
+ // Rotating right increases the counter, rotating left decreases it.
+ // Pressing the dial resets the counter to zero.
+
+ public class CounterAdjustment : PluginDynamicAdjustment
+ {
+ // Holds the current value of the counter.
+ private Int32 _counter = 0;
+
+ // Initializes the adjustment.
+ // hasReset: true automatically creates a reset button for this adjustment.
+ public CounterAdjustment()
+ : base(displayName: "Counter", description: "Counts rotation ticks of the dial", groupName: "Adjustments", hasReset: true)
+ {
+ }
+
+ // This method is called when the dial is rotated.
+ // diff is positive when rotating right, negative when rotating left.
+ protected override void ApplyAdjustment(String actionParameter, Int32 diff)
+ {
+ // Increase or decrease the counter by the number of ticks.
+ this._counter += diff;
+
+ // Notify the Plugin Service that the value has changed.
+ this.AdjustmentValueChanged();
+
+ PluginLog.Info($"Counter changed by {diff}, new value: {this._counter}");
+ }
+
+ // This method is called when the reset button is pressed.
+ protected override void RunCommand(String actionParameter)
+ {
+ // Reset the counter back to zero.
+ this._counter = 0;
+
+ // Notify the Plugin Service that the value has changed.
+ this.AdjustmentValueChanged();
+
+ PluginLog.Info("Counter reset to 0");
+ }
+
+ // Returns the current counter value shown next to the dial.
+ protected override String GetAdjustmentValue(String actionParameter) => this._counter.ToString();
+ }
+}
diff --git a/DemoPlugin/src/Actions/ThumbUpDownCommand.cs b/DemoPlugin/src/Actions/ThumbUpDownCommand.cs
new file mode 100644
index 0000000..51c8129
--- /dev/null
+++ b/DemoPlugin/src/Actions/ThumbUpDownCommand.cs
@@ -0,0 +1,44 @@
+namespace Loupedeck.DemoPlugin
+{
+ using System;
+
+ // This command toggles between a Thumb Up and Thumb Down image on the button.
+ // It demonstrates how to use embedded image resources for button icons.
+
+ public class ThumbUpDownCommand : PluginDynamicCommand
+ {
+ // Tracks whether the current state is thumb down or thumb up.
+ private Boolean _isThumbDown = false;
+
+ // Resource paths for the thumb up and thumb down images.
+ private readonly String _imageResourcePathThumbUp;
+ private readonly String _imageResourcePathThumbDown;
+
+ // Initializes the command and loads the embedded image resource paths.
+ public ThumbUpDownCommand()
+ : base(displayName: "Thumb Up/Down", description: "Toggles between thumb up and thumb down", groupName: "Switches")
+ {
+ // Find the embedded image resources by filename.
+ this._imageResourcePathThumbUp = PluginResources.FindFile("ThumbUp.png");
+ this._imageResourcePathThumbDown = PluginResources.FindFile("ThumbDown.png");
+ }
+
+ // This method is called when the user presses the button.
+ protected override void RunCommand(String actionParameter)
+ {
+ // Toggle the thumb state.
+ this._isThumbDown = !this._isThumbDown;
+
+ // Notify the Plugin Service to refresh the button image.
+ this.ActionImageChanged();
+ }
+
+ // Returns the correct image based on the current thumb state.
+ protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize)
+ {
+ // Show ThumbDown image when down, ThumbUp image when up.
+ var resourcePath = this._isThumbDown ? this._imageResourcePathThumbDown : this._imageResourcePathThumbUp;
+ return PluginResources.ReadImage(resourcePath);
+ }
+ }
+}
diff --git a/DemoPlugin/src/Actions/ToggleMuteCommand.cs b/DemoPlugin/src/Actions/ToggleMuteCommand.cs
new file mode 100644
index 0000000..704fcc7
--- /dev/null
+++ b/DemoPlugin/src/Actions/ToggleMuteCommand.cs
@@ -0,0 +1,47 @@
+namespace Loupedeck.DemoPlugin
+{
+ using System;
+ using System.Runtime.InteropServices;
+
+ // This command toggles the system mute on both macOS and Windows.
+ // It is a universal command (no application linked) so it works globally.
+
+ public class ToggleMuteCommand : PluginDynamicCommand
+ {
+ // Tracks the current mute state to update the button text accordingly.
+ private Boolean _isMuted = false;
+
+ // Initializes the command with display name, description, and group.
+ public ToggleMuteCommand()
+ : base(displayName: "Toggle Mute", description: "Mutes and unmutes system volume", groupName: "Audio")
+ {
+ }
+
+ // This method is called when the user presses the button assigned to this command.
+ protected override void RunCommand(String actionParameter)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ // On macOS, use osascript to toggle the mute state.
+ // Reads the current mute state and flips it.
+ System.Diagnostics.Process.Start("osascript", "-e \"set volume output muted not (output muted of (get volume settings))\"");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // On Windows, use PowerShell to send the VolumeMute key (keycode 173).
+ System.Diagnostics.Process.Start("powershell", "-c \"(New-Object -ComObject WScript.Shell).SendKeys([char]173)\"");
+ }
+
+ // Flip the mute state and refresh the button label.
+ this._isMuted = !this._isMuted;
+ this.ActionImageChanged();
+ }
+
+ // Returns the button label based on the current mute state.
+ protected override String GetCommandDisplayName(String actionParameter, PluginImageSize imageSize)
+ {
+ // Show "MUTED" when muted, "UNMUTED" when sound is on.
+ return this._isMuted ? "Mute" : "Unmute";
+ }
+ }
+}
diff --git a/DemoPlugin/DemoPlugin/DemoApplication.cs b/DemoPlugin/src/DemoApplication.cs
similarity index 53%
rename from DemoPlugin/DemoPlugin/DemoApplication.cs
rename to DemoPlugin/src/DemoApplication.cs
index b73d462..377cd8b 100644
--- a/DemoPlugin/DemoPlugin/DemoApplication.cs
+++ b/DemoPlugin/src/DemoApplication.cs
@@ -2,6 +2,8 @@ namespace Loupedeck.DemoPlugin
{
using System;
+ // This class can be used to connect the Loupedeck plugin to an application.
+
public class DemoApplication : ClientApplication
{
public DemoApplication()
@@ -9,9 +11,12 @@ public DemoApplication()
}
// This method can be used to link the plugin to a Windows application.
- protected override String GetProcessName() => "DemoApplication";
+ protected override String GetProcessName() => "";
// This method can be used to link the plugin to a macOS application.
protected override String GetBundleName() => "";
+
+ // This method can be used to check whether the application is installed or not.
+ public override ClientApplicationStatus GetApplicationStatus() => ClientApplicationStatus.Unknown;
}
}
diff --git a/DemoPlugin/DemoPlugin/DemoPlugin.cs b/DemoPlugin/src/DemoPlugin.cs
similarity index 92%
rename from DemoPlugin/DemoPlugin/DemoPlugin.cs
rename to DemoPlugin/src/DemoPlugin.cs
index 3582542..4b9e14b 100644
--- a/DemoPlugin/DemoPlugin/DemoPlugin.cs
+++ b/DemoPlugin/src/DemoPlugin.cs
@@ -2,6 +2,8 @@ namespace Loupedeck.DemoPlugin
{
using System;
+ // This class contains the plugin-level logic of the Loupedeck plugin.
+
public class DemoPlugin : Plugin
{
// Gets a value indicating whether this is an API-only plugin.
diff --git a/DemoPlugin/DemoPlugin/DemoPlugin.csproj b/DemoPlugin/src/DemoPlugin.csproj
similarity index 64%
rename from DemoPlugin/DemoPlugin/DemoPlugin.csproj
rename to DemoPlugin/src/DemoPlugin.csproj
index 4cdaddb..3bd0672 100644
--- a/DemoPlugin/DemoPlugin/DemoPlugin.csproj
+++ b/DemoPlugin/src/DemoPlugin.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
@@ -18,17 +18,20 @@
$(LocalAppData)\Logi\LogiPluginService\Plugins\
~/Library/Application\ Support/Logi/LogiPluginService/Plugins/
+ $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\bin\'))
$(BaseOutputPath)$(Configuration)\bin\
+
+ Demo
-
+
-
+
@@ -38,22 +41,34 @@
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/DemoPlugin/Directory.Build.props b/DemoPlugin/src/Directory.Build.props
similarity index 74%
rename from DemoPlugin/Directory.Build.props
rename to DemoPlugin/src/Directory.Build.props
index 287f0b1..fccb8a7 100644
--- a/DemoPlugin/Directory.Build.props
+++ b/DemoPlugin/src/Directory.Build.props
@@ -2,6 +2,6 @@
latest
$(SolutionDir)obj\
- $(SolutionDir)bin\
+ $(SolutionDir)..\bin\
diff --git a/DemoPlugin/DemoPlugin/PluginLog.cs b/DemoPlugin/src/Helpers/PluginLog.cs
similarity index 94%
rename from DemoPlugin/DemoPlugin/PluginLog.cs
rename to DemoPlugin/src/Helpers/PluginLog.cs
index b10542d..760eb8a 100644
--- a/DemoPlugin/DemoPlugin/PluginLog.cs
+++ b/DemoPlugin/src/Helpers/PluginLog.cs
@@ -2,7 +2,7 @@ namespace Loupedeck.DemoPlugin
{
using System;
- // A helper class to write to the plugin log.
+ // A helper class that enables logging from the plugin code.
internal static class PluginLog
{
diff --git a/DemoPlugin/DemoPlugin/PluginResources.cs b/DemoPlugin/src/Helpers/PluginResources.cs
similarity index 100%
rename from DemoPlugin/DemoPlugin/PluginResources.cs
rename to DemoPlugin/src/Helpers/PluginResources.cs
diff --git a/DemoPlugin/DemoPlugin/images/ThumbDown.png b/DemoPlugin/src/images/ThumbDown.png
similarity index 100%
rename from DemoPlugin/DemoPlugin/images/ThumbDown.png
rename to DemoPlugin/src/images/ThumbDown.png
diff --git a/DemoPlugin/DemoPlugin/images/ThumbUp.png b/DemoPlugin/src/images/ThumbUp.png
similarity index 100%
rename from DemoPlugin/DemoPlugin/images/ThumbUp.png
rename to DemoPlugin/src/images/ThumbUp.png
diff --git a/DemoPlugin/src/images/VolumeOff.png b/DemoPlugin/src/images/VolumeOff.png
new file mode 100644
index 0000000..5a6f0b3
Binary files /dev/null and b/DemoPlugin/src/images/VolumeOff.png differ
diff --git a/DemoPlugin/src/images/VolumeOn.png b/DemoPlugin/src/images/VolumeOn.png
new file mode 100644
index 0000000..11766a6
Binary files /dev/null and b/DemoPlugin/src/images/VolumeOn.png differ
diff --git a/DemoPlugin/DemoPlugin/package/metadata/Icon256x256.png b/DemoPlugin/src/package/metadata/Icon256x256.png
similarity index 100%
rename from DemoPlugin/DemoPlugin/package/metadata/Icon256x256.png
rename to DemoPlugin/src/package/metadata/Icon256x256.png
diff --git a/DemoPlugin/src/package/metadata/LoupedeckPackage.yaml b/DemoPlugin/src/package/metadata/LoupedeckPackage.yaml
new file mode 100644
index 0000000..d171daa
--- /dev/null
+++ b/DemoPlugin/src/package/metadata/LoupedeckPackage.yaml
@@ -0,0 +1,80 @@
+# ==================================================================================================
+# GENERAL SETTINGS
+# ==================================================================================================
+
+# Package type. Must be plugin4 for plugins.
+type: plugin4
+
+# Name that identifies the plugin.
+name: Demo
+
+# Plugin name that is shown to the user.
+displayName: Demo
+
+# Short description of the plugin.
+description: Add a short description of the plugin here.
+
+# Name of the plugin library file.
+pluginFileName: DemoPlugin.dll
+
+# Plugin version.
+version: 1.0
+
+# Author of the plugin. The author can be a company or an individual developer.
+author: nirmal
+
+# Copyright information.
+copyright: Copyright © 2026 nirmal. All rights reserved.
+
+
+# ==================================================================================================
+# PLUGIN PROPERTIES
+# ==================================================================================================
+
+# Location of plugin files on Windows (relative to the plugin base directory).
+# This parameter is required to support Windows.
+pluginFolderWin: bin
+
+# Location of plugin files on macOS (relative to the plugin base directory).
+# This parameter is required to support Mac.
+pluginFolderMac: bin
+
+# List of devices the plugin is optimized for.
+supportedDevices:
+ # LoupedeckCtFamily covers the following devices: Loupedeck CT, Live and Live S,
+ # Razer Stream Controller, and Razer Stream Controller X.
+ - LoupedeckCtFamily
+
+ # LoupedeckPlusFamily covers Loupedeck+ device. Uncomment the following line to support Loupedeck+.
+ #- LoupedeckPlusFamily
+
+# List of plugin capabilities.
+pluginCapabilities:
+ # Uncomment the following line if this plugin is an application plugin.
+ #- HasApplication
+
+ # Uncomment the following line if the plugin sends keyboard shortcuts to the target application.
+ #- ActivatesApplication
+
+# Minimum Loupedeck version supported by the plugin.
+minimumLoupedeckVersion: 6.0
+
+
+# ==================================================================================================
+# LOUPEDECK MARKETPLACE SETTINGS
+# ==================================================================================================
+
+# Name of the license that the plugin is licensed under. Select the one that you prefer.
+# NOTE: GPL license is not compatible with Loupedeck Marketplace.
+license: MIT
+
+# URL of the plugin license.
+licenseUrl: https://opensource.org/licenses/MIT
+
+# URL of the support page where the users can send improvement suggestions and report bugs.
+# The URL is shown in Loupedeck Marketplace. The page can be for example a GitHub issues page.
+# NOTE: This setting is recommended when publishing the plugin in Loupedeck Marketplace.
+#supportPageUrl: https://github.com/myusername/myplugin/issues
+
+# URL of the plugin homepage. The URL is shown in Loupedeck Marketplace.
+#homePageUrl: https://github.com/myusername/myplugin
diff --git a/README.md b/README.md
index f35178d..1181174 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,73 @@
# Welcome to Logi Actions SDK
-Here you can find an introduction to Logi Actions SDK, example plugin source code and the SDK documentation.
+Here you can find an introduction to Logi Actions SDK, example plugin source code and the SDK documentation.
## Documentation
-* [Logi Actions SDK Developer Docs](https://logitech.github.io/actions-sdk-docs)
+- [Logi Actions SDK Developer Docs](https://logitech.github.io/actions-sdk-docs/)
+- [Logi Developer Discord](https://discord.gg/ptV2BfHCmm)
-## Developer Discord
+---
-* [Logi Developer Discord](https://discord.gg/etJCPZytHg)
+# DemoPlugin
+A Logitech Actions SDK plugin built with C# (.NET 8) that works on both macOS and Windows. This project is intended as a learning resource for new developers joining the team — each file is structured and commented to help you understand how the SDK works.
+
+## Folder Structure
+
+```
+DemoPlugin/
+├── .vscode/ # VS Code setup — debugger config to attach to the Logi Plugin Service
+│ # and build tasks for quick builds, packaging and installing
+├── src/
+│ ├── Actions/ # Where all commands and adjustments live.
+│ # Each file is one feature assigned to a button or dial
+│ ├── Helpers/ # Shared utility classes used across all actions.
+│ # PluginLog for logging, PluginResources for loading images
+│ ├── images/ # PNG icons embedded into the plugin DLL.
+│ # These are displayed on device buttons at runtime
+│ └── package/ # Plugin identity files read by the Logi Plugin Service
+│ # on startup — name, version, icon and supported devices
+└── bin/ # Auto-generated build output. Never edit this folder manually.
+```
+
+## Actions
+
+| Action | Type | Description |
+|---|---|---|
+| Toggle Mute | Button | Mutes and unmutes system volume on macOS and Windows |
+| Button Switches | Button | 4 independent on/off toggle switches |
+| Thumb Up/Down | Button | Toggles between thumb up and thumb down image |
+| Counter | Dial | Counts rotation ticks, press dial to reset |
+
+## Build
+
+```bash
+# Debug — used during development and debugging
+dotnet build -c Debug
+
+# Release — used for packaging and distribution
+dotnet build -c Release
+```
+
+## Debug
+
+1. Open the project in VS Code
+2. Run `dotnet build -c Debug`
+3. Go to **Run > Start Debugging** (or press `F5`)
+4. Select **Attach to Logi Plugin Service**
+
+You can now set breakpoints in any action file and step through your code live.
+
+## Package & Install
+
+```bash
+# Package the plugin into a distributable .lplug4 file
+logiplugintool pack ./bin/Release ./Demo.lplug4
+
+# Install the plugin on this machine
+logiplugintool install ./Demo.lplug4
+
+# Uninstall the plugin
+logiplugintool uninstall Demo
+```