# Touch Dial Plugin Development Template

---

## ⚖️ Developer Disclaimer & Usage Policy

> **IMPORTANT — READ BEFORE DEVELOPING OR DISTRIBUTING ANY PLUGIN**

### Liability Exclusion

This SDK and documentation are provided **"as-is"** without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors, copyright holders, or Touch Dial be liable for any claim, damages, or other liability — whether in an action of contract, tort, or otherwise — arising from, out of, or in connection with the SDK, this documentation, any plugin developed using them, or any use or other dealings therein.

### Purpose & Scope

This SDK is provided for the **sole purpose** of enabling general-purpose software extension and UI customization. Touch Dial is a neutral, general-purpose platform that processes **standard input signals** (HID peripherals, MIDI controllers, mouse/scroll events, touch inputs, keyboard events) and renders **dynamic overlay interfaces** in response. The SDK accepts data representing incremental value changes and state transitions from any compatible input source.

Touch Dial does not provide, support, or endorse plugins designed to:

- Circumvent technical protection measures (TPMs) or digital rights management (DRM).
- Reverse-engineer, replicate, or emulate proprietary protocols, hardware behaviors, or system drivers.
- Infringe upon any third-party intellectual property rights, including patents, copyrights, trademarks, or trade secrets.
- Bypass, defeat, or work around hardware authentication, licensing mechanisms, or access controls.

### Developer Responsibility

By using this SDK, you acknowledge and agree that:

1. **You are solely responsible** for ensuring that your plugin implementations comply with all applicable local, national, and international patent, copyright, trademark, and trade secret laws.
2. **You must independently evaluate** whether your plugin's functionality may implicate any third-party intellectual property rights before development, distribution, or use.
3. **Touch Dial bears no liability** for any direct, indirect, contributory, or vicarious infringement claims arising from your use of this SDK or distribution of plugins built with it.
4. **Use of this SDK to replicate patented hardware behaviors**, proprietary input processing methods, or protected system drivers is **strictly prohibited** under the terms of the EULA.

### Platform Neutrality & Separation of Concerns

Touch Dial is architecturally designed as an **input-agnostic data pipeline**:

- The host application processes **abstract signals** (e.g., incremental value changes, button state transitions) and renders corresponding UI responses.
- The host does not inspect, validate, or assume the origin of input signals received from plugins.
- All input-source-specific logic is **isolated within plugin code** and is the sole responsibility of the plugin developer.
- The SDK defines **standard interfaces** — plugins provide data; the host renders UI. This separation means the host application has no knowledge of, and assumes no liability for, how a plugin generates its input signals.

### Intended Non-Infringing Uses

This SDK is designed for **substantial non-infringing purposes**, including but not limited to:

- **Productivity shortcuts** — mapping keyboard macros, application launchers, or workflow automations to customizable UI elements.
- **Media control** — volume adjustment, play/pause, track navigation, and brightness mapping using standard system APIs.
- **Creative tools** — brush size, opacity, zoom, or timeline scrubbing driven by standard mouse-wheel, scroll, or touch events.
- **Accessibility** — custom input mappings for users with motor or sensory disabilities.
- **System monitoring** — displaying CPU, memory, network, or other telemetry data in an overlay interface.

### Community & Distribution Policy

If you distribute plugins through any public channel (forums, repositories, marketplaces, or social media):

- **Do not** share or promote plugins that bypass hardware protections, circumvent licensing, or mimic specific patented devices.
- **Do not** describe or market plugins as replacements for, or workarounds to, specific proprietary hardware or software products.
- Use **industry-standard, generic terminology** (e.g., "input event," "overlay interface," "value change signal") rather than references to specific brands, proprietary hardware names, or patented technologies.

### Governing Terms

This disclaimer supplements and does not replace the Touch Dial End User License Agreement (EULA). In the event of a conflict, the EULA governs. By using this SDK, you agree to be bound by both this disclaimer and the EULA.

### Security Model & Plugin Trust

> ⚠️ **Plugins run with the full permissions of the current user account.** Third-party plugins load into a separate host process (`TouchDial.PluginHost.exe`), which isolates them from the main application's (native-AOT) process — but they are **not security-sandboxed**: a plugin can access the file system, network, clipboard, and Win32 APIs to the same extent as any desktop application the user runs, and it shares the host process with other third-party plugins.

**For plugin developers:**
- Use `context.Sandbox` for file I/O within your plugin directory — it validates paths and rejects symlink/junction escapes.
- Use `context.GetSharedPath("subfolder")` for user-visible shared data under `Documents\TouchDial\`.
- Do not access other plugins' directories or the host application's config files directly.

**For end users:**
- Only install plugins from sources you trust. A malicious plugin could read, modify, or delete files accessible to your user account.
- The `.tdial` plugin format is a ZIP archive containing compiled .NET code — treat it with the same caution as any executable.

---

This is the complete guide for creating a **Plugin (`.tdial`)** for the Touch Dial application. A plugin can appear in **two places**: the **Settings > Plugins** list (with an inline settings UI panel), and the **Select Action > Plugins** tab (where users bind plugin actions to wheel buttons).

> **Required file**: You need the `TouchDial.Sdk_v2.0.dll` file to compile your plugin. This is the only dependency from the host application.

---

## 0. Requirements to building plugins:

  Minimum Requirements

  ┌────────────────────────┬───────────────────────────────────────────────────┐
  │ Requirement            │ Details                                           │
  ├────────────────────────┼───────────────────────────────────────────────────┤
  │ .NET 10 SDK            │ Any 10.0.x version (dotnet --version to check)    │
  ├────────────────────────┼───────────────────────────────────────────────────┤
  │ Windows 10 SDK 22621   │ Included with Visual Studio or standalone install │
  ├────────────────────────┼───────────────────────────────────────────────────┤
  │ TouchDial.Sdk_v2.0.dll │ Provided.                                         │
  └────────────────────────┴───────────────────────────────────────────────────┘

  NuGet Package (auto-restored)

   - Microsoft.WindowsAppSDK 1.8.260529003 — needed for WinUI 3 UI types (Grid, Button, Window, etc.)

## 1. Architecture Overview

A `.tdial` file is a **ZIP archive** (renamed to `.tdial`) containing:
- A compiled .NET 10 DLL (your plugin)
- A `manifest.json` file
- Optional assets (icons, etc.)
- Optional dependency DLLs (but **NOT** `TouchDial.Sdk_v2.0.dll` — the host provides this at runtime)

The host application loads your DLL, finds the class specified in `manifest.json`, and calls the `IPlugin` interface methods.

### Where Plugins Appear

| Location | How it works | What you implement |
|---|---|---|
| **Settings > Plugins list** | Your plugin appears as a row (icon + name + description). When clicked, your settings UI panel expands below. | `GetSettingsUI()` returns a WinUI 3 control |
| **Select Action > Plugins tab** | Users see your registered actions and can assign them to wheel buttons. When the wheel button is pressed, your action handler runs. | `_context.RegisterAction(...)` in `Initialize()` |

Both are controlled from the same plugin class. A plugin **must** implement both to be fully functional.

---

## 1a. Plugin Execution Model (Out-of-Process) — READ THIS

> **What changed:** As of the Native-AOT release, the Touch Dial main application is compiled to
> **native machine code** (for code protection) and therefore **cannot load .NET IL plugin
> assemblies in-process**. Instead, third-party plugins now run in a **separate host process**,
> `TouchDial.PluginHost.exe`, which ships inside the app and provides a full **bundled .NET 10
> runtime**. Your plugin is JIT-compiled and run there, and talks to the main app over IPC.

**What this means for you (the plugin author):**

- **Build framework-dependent** — a normal `.NET 10` class library. **Do NOT** publish self-contained
  and **do NOT** enable `PublishAot`. The host supplies the .NET runtime; your `.tdial` only needs
  your own DLL(s) + `manifest.json` (+ your own NuGet dependencies).
- **Reference `TouchDial.Sdk` with `<Private>false</Private>`** (unchanged) — the host provides the
  SDK at runtime. Shipping your own copy breaks type identity and your plugin won't load.
- The IPC is **transparent to you** — you keep using the normal `PluginContext` API. Calls such as
  `context.GetMainWindowBounds()` are tunneled back to the main app automatically.
- Your plugin shares the host process with other third-party plugins (see Security Model above).

### ✅ Current Support Status

| Capability | API | Status |
|---|---|---|
| **Action plugins** (bind to wheel buttons) | `context.RegisterAction(id, name, handler)` in `Initialize()` | ✅ **Supported.** Appears under **Select Action > Plugins**. |
| **Inline settings panel** | `ISettingsProvider` (declarative schema) | ✅ **Supported.** Rendered natively & inline in **Settings > Plugins** (recommended — see §1b). |
| **Dial-following plugin window** | `context.RegisterWindow(window)` | ✅ **Supported.** Your WinUI window renders in the host and follows the dial. |
| **Windowed settings** | `GetSettingsUI()` returning a WinUI control | ✅ **Supported, and now rendered INLINE** in **Settings > Plugins** via cross-process airspace embedding (the host paints your control over the reserved panel region). Use this for rich/custom settings UI; use `ISettingsProvider` when a simple declarative form is enough. |

> **Bottom line:** action plugins, inline settings (both `ISettingsProvider` **and** a rich
> `GetSettingsUI()` control), and dial-following windows (`RegisterWindow`) all work end-to-end
> out-of-process, rendered inline in the main app.

### 1b. Inline settings: `ISettingsProvider` (declarative) vs `GetSettingsUI()` (rich)

Both approaches show **inline** in **Settings > Plugins** (like the built-in plugins). Pick one:

- **`ISettingsProvider`** — describe your settings **declaratively** and the main app renders native
  controls for you, reporting edits back. Best for simple forms (toggles, text, sliders, dropdowns).
  No cross-process UI concerns. **Recommended when it's enough.**
- **`GetSettingsUI()`** — return your own WinUI 3 control for a fully custom panel. The host renders it
  and the main app embeds it inline over the reserved region (cross-process airspace embedding). Use
  this when you need custom layout/visuals the schema can't express. Build it **purely in code**
  (no XAML files — see §2). If you implement **both**, the rich `GetSettingsUI()` panel is used.

Implement `ISettingsProvider` **in addition to** `IPlugin`:

```csharp
public class MyPlugin : IPlugin, ISettingsProvider
{
    private bool _enabled = true;
    private string _name = "world";
    private double _level = 50;

    public PluginSettingsSchema GetSettingsSchema() => new()
    {
        Fields =
        {
            new PluginSettingField { Type = PluginSettingType.Label,  Label = "My Plugin settings" },
            new PluginSettingField { Key = "enabled", Type = PluginSettingType.Toggle,   Label = "Enabled", Bool = _enabled },
            new PluginSettingField { Key = "name",    Type = PluginSettingType.Text,     Label = "Name",    Text = _name },
            new PluginSettingField { Key = "level",   Type = PluginSettingType.Slider,   Label = "Level",   Number = _level, Min = 0, Max = 100, Step = 1 },
            new PluginSettingField { Key = "mode",    Type = PluginSettingType.Dropdown, Label = "Mode",    Text = "B", Options = { "A", "B", "C" } },
        }
    };

    // value: Toggle -> "True"/"False", Number/Slider -> invariant double, Text/Dropdown -> the text,
    // Button -> "". Persist the change in your own settings store.
    public void OnSettingChanged(string key, string value)
    {
        switch (key)
        {
            case "enabled": _enabled = value == "True"; break;
            case "name":    _name = value; break;
            case "level":   _level = double.Parse(value, System.Globalization.CultureInfo.InvariantCulture); break;
        }
        // ...save to disk...
    }
}
```

Field types: `Label`, `Toggle`, `Text`, `Number`, `Slider`, `Dropdown`, `Button`. Build the schema with
the **current** values so the panel shows the right state. You may implement **both** `ISettingsProvider`
(declarative) and `GetSettingsUI()` (rich custom control) — both render inline; the rich
`GetSettingsUI()` panel takes precedence in the Settings list.

### 1c. Plugin windows: toggle, follow & border color

If your plugin opens its own window (via `context.RegisterWindow(window)`), follow these three rules so
it behaves like a first-class dial plugin:

**1. Toggle on second press.** A plugin action bound to a wheel button runs your handler each time the
button is pressed. Open your window on the first press and **close it on the second** — track the window
yourself:

```csharp
private MyWindow? _window;

context.RegisterAction("toggle_myplugin", "Open My Plugin", () =>
{
    if (_window != null) { _window.Close(); _window = null; return; }   // 2nd press: close
    _window = new MyWindow(context);
    _window.Closed += (s, e) => _window = null;
    context.RegisterWindow(_window);   // host shows it + makes it follow the dial
    _window.Activate();
});
```

**2. Following the dial is automatic.** Once you call `context.RegisterWindow(window)`, the host keeps
the window positioned next to the dial (and moving with it, no lag/jitter) — you don't write any
positioning code. Pass `WindowShape.Circular` as the second arg for circular windows.

**3. Match the dial's border color.** Set your window's border from `context.OuterRingR/G/B` and update
it live by subscribing to `OuterRingColorChanged` (remember to unsubscribe on close):

```csharp
// initial color
var c = Windows.UI.Color.FromArgb(255, context.OuterRingR, context.OuterRingG, context.OuterRingB);
_border.BorderBrush = new SolidColorBrush(c);

// live updates (fires on the UI thread)
void OnColor(byte r, byte g, byte b) =>
    _border.BorderBrush = new SolidColorBrush(Windows.UI.Color.FromArgb(255, r, g, b));
context.OuterRingColorChanged += OnColor;
window.Closed += (s, e) => context.OuterRingColorChanged -= OnColor;
```

**What the host guarantees for your window (you get these for free):**

- **Always on top.** Registered plugin windows are kept above other applications (like the dial
  itself), so your window won't disappear behind the foreground app. You do **not** need to set
  `IsAlwaysOnTop` or call `SetWindowPos` yourself — the host re-asserts top-most continuously.
- **No taskbar / Alt-Tab entry.** Plugin windows are treated as dial accessories, not standalone
  apps, so they don't clutter the taskbar or the Alt-Tab switcher.
- **Reliable open → close → reopen.** The host process stays alive even when all plugin windows are
  closed, so your toggle action keeps working indefinitely (open, close, open again, …). Just track
  your window reference as shown above and create a fresh window on each open.

### UI rule

Plugin UI **must be built entirely in C#** — **no XAML resource files**. WinUI 3 cannot resolve XAML
resources across a dynamically-loaded DLL boundary, and this is doubly true across a process
boundary. Build all controls in code (see the `GetSettingsUI()` examples below).

---

## 2. SDK Reference (Complete API)

### 2.1 IPlugin Interface

This is the contract your main class must implement:

```csharp
namespace TouchDial.Sdk
{
    public interface IPlugin
    {
        /// Called once when the plugin is loaded. Register actions and setup here.
        void Initialize(PluginContext context);

        /// Called when the user enables the plugin or on app start if previously enabled.
        void OnEnable();

        /// Called when the user disables the plugin.
        void OnDisable();

        /// Called when the app closes or the plugin is uninstalled. Clean up resources.
        void Terminate();

        /// Return a WinUI 3 UIElement for the Settings > Plugins panel, or null.
        /// IMPORTANT: Build UI in pure C# (inherit from Grid/StackPanel), NOT XAML files.
        /// XAML resource resolution fails across DLL boundaries in WinUI 3.
        object GetSettingsUI();
    }
}
```

### 2.2 PluginContext Class

Provided to your plugin via `Initialize()`. This is your gateway to the host application:

```csharp
namespace TouchDial.Sdk
{
    public class PluginContext
    {
        /// Absolute path to your plugin's installation directory.
        /// Use this to find your assets, save settings files, etc.
        public string InstallationPath { get; }

        /// Path to plugin.log in your installation directory.
        public string LogFilePath { get; }

        /// Write an INFO log entry to plugin.log.
        public void Log(string message);

        /// Write an ERROR log entry to plugin.log.
        public void LogError(string message, Exception? ex = null);

        /// Register a custom action that appears in Select Action > Plugins tab.
        /// Users can bind this to any wheel button.
        /// - id: Unique ID within your plugin (e.g., "toggle_window")
        /// - name: Display name shown to the user (e.g., "Open My Tool")
        /// - handler: Code that runs when the wheel button is pressed
        public void RegisterAction(string id, string name, Action handler);

        /// All actions registered by this plugin.
        public IReadOnlyList<PluginActionDefinition> RegisteredActions { get; }

        /// Delegate to get the main wheel window's screen bounds (X, Y, Width, Height).
        /// Use this to position your windows relative to the wheel.
        public Func<(int X, int Y, int Width, int Height)>? GetMainWindowBounds { get; set; }

        /// Delegate to get the main wheel window's native handle (HWND).
        /// Use this to create child windows (WS_CHILD) that move with the dial
        /// automatically at the OS level — zero positioning lag.
        /// Returns IntPtr.Zero if the window is not yet created.
        public Func<IntPtr>? GetMainWindowHandle { get; set; }

        /// Register a window with the host for automatic positioning.
        /// The host will position it next to the wheel and move it with
        /// "Follow Dial" if the user has enabled that option.
        public void RegisterWindow(object window);

        /// Register a window with a shape hint. Circular windows automatically
        /// receive anti-aliased edge rendering from the host (same smooth ring
        /// used on the main dial). Rectangular is the default.
        public void RegisterWindow(object window, WindowShape shape);

        /// The currently selected UI language (display name).
        /// Updated in real-time when the user changes the language in Settings.
        /// Supported values: "English", "Deutsch", "Français", "Español", "Italiano", "Português", "中文", "हिन्दी"
        public string CurrentLanguage { get; set; }

        /// Fired whenever the user changes the language in Settings.
        /// The string parameter is the new language display name.
        /// Subscribe to this in Initialize() to update your plugin's UI translations live.
        public event Action<string>? LanguageChanged;

        /// The current Outer Ring color (R, G, B components, 0-255).
        /// Updated in real-time when the user changes the color in Settings or during rainbow animation.
        /// Includes brightness adjustment already applied.
        public byte OuterRingR { get; }
        public byte OuterRingG { get; }
        public byte OuterRingB { get; }

        /// Sets the outer ring color. Called by the host — plugins should not call this directly.
        public void SetOuterRingColor(byte r, byte g, byte b);

        /// Fired whenever the Outer Ring color changes (from Settings or rainbow animation).
        /// Parameters are R, G, B (0-255). Subscribe to this in Initialize() to update
        /// your plugin's border/theme colors to match the main dial.
        public event Action<byte, byte, byte>? OuterRingColorChanged;

        /// Sandboxed file-system helper. Use this to read/write files safely
        /// within your plugin's installation directory. Paths that escape the
        /// sandbox will throw UnauthorizedAccessException.
        /// Rejects symbolic links and junctions that could redirect outside the sandbox.
        public PluginSandbox Sandbox { get; }

        /// Returns a validated path under Documents\TouchDial\{subfolder}\ for shared data
        /// that should live outside the plugin directory (e.g., user-visible files).
        /// The subfolder is created automatically. Only simple names allowed — no path separators.
        /// Example: context.GetSharedPath("Notes") → "C:\Users\...\Documents\TouchDial\Notes\"
        public string GetSharedPath(string subfolder);
    }
}
```

### 2.3 PluginActionDefinition Class

```csharp
namespace TouchDial.Sdk
{
    public class PluginActionDefinition
    {
        public string Id { get; }           // Unique action ID within the plugin
        public string Name { get; }         // User-facing display name
        public Action Handler { get; }      // Code to execute
        public string OwnerPluginId { get; set; } // Set automatically by host
    }
}
```

### 2.4 PluginManifest Class

The host deserializes your `manifest.json` into this class:

```csharp
namespace TouchDial.Sdk
{
    public class PluginManifest
    {
        public string Id { get; set; }          // e.g., "com.yourname.pluginname"
        public string Name { get; set; }        // Display name in Settings
        public string Version { get; set; }     // e.g., "1.0.0"
        public string Description { get; set; } // Shown below name in plugin list
        public string Author { get; set; }      // Author name
        public string EntryPoint { get; set; }  // Your DLL filename, e.g., "MyPlugin.dll"
        public string EntryClass { get; set; }  // Full class name, e.g., "MyNamespace.MyPlugin"
        public string Icon { get; set; }        // Relative path to icon, e.g., "icon.png"
    }
}
```


### 2.5 PluginSandbox Class

```csharp
namespace TouchDial.Sdk
{
    public sealed class PluginSandbox
    {
        /// The root directory this plugin is allowed to access.
        public string Root { get; }

        /// Validates that the path stays inside the sandbox. Throws if it escapes.
        public string ValidatePath(string relativePath);

        /// Returns true if the path stays inside the sandbox.
        public bool IsPathAllowed(string relativePath);

        /// Read a text file inside the plugin directory.
        public string ReadText(string relativePath);

        /// Write a text file inside the plugin directory.
        public void WriteText(string relativePath, string content);

        /// Read all bytes from a file inside the plugin directory.
        public byte[] ReadBytes(string relativePath);

        /// Write bytes to a file inside the plugin directory.
        public void WriteBytes(string relativePath, byte[] data);

        /// Check if a file exists inside the plugin directory.
        public bool FileExists(string relativePath);

        /// Delete a file inside the plugin directory.
        public void DeleteFile(string relativePath);
    }
}
```

### 2.6 WindowShape Enum

Describes the shape of a plugin window. Pass this to `RegisterWindow()` so the host can apply appropriate visual treatments automatically.

```csharp
namespace TouchDial.Sdk
{
    public enum WindowShape
    {
        Rectangular,  // Default — standard rectangular window
        Circular      // Circular window using SetWindowRgn / CreateEllipticRgn.
                      // The host automatically adds an anti-aliased edge ring overlay.
    }
}
```

**Usage:**
```csharp
// In your plugin's window creation code:
_context.RegisterWindow(myWindow, WindowShape.Circular);
```

When `WindowShape.Circular` is specified, the host creates a companion overlay window that renders a smooth anti-aliased ring at the edge of the circular GDI region, eliminating the jagged pixel staircase effect. The ring follows the window automatically during movement.

### 2.7 HostWindows — Overlay Window Filtering

The host application uses invisible overlay windows (anti-aliased ring borders) for visual polish. These windows are tagged with a Win32 property so plugins can identify and skip them. **Any plugin that uses `EnumWindows` must filter overlay windows** to avoid false detections.

```csharp
namespace TouchDial.Sdk
{
    public static class HostWindows
    {
        // Returns true if the window is a visual overlay that plugins should ignore.
        public static bool IsOverlayWindow(IntPtr hwnd);

        // Tags a plugin-created overlay window so other plugins will skip it.
        public static void TagAsOverlay(IntPtr hwnd);
    }
}
```

**Filtering (required in any EnumWindows callback):**
```csharp
private bool EnumWindowsCallback(IntPtr hWnd, IntPtr lParam)
{
    if (HostWindows.IsOverlayWindow(hWnd)) return true; // skip overlay
    // ... your detection logic ...
}
```

**Tagging (required if your plugin creates its own overlay windows):**
```csharp
// After creating a layered overlay window (e.g. your own AA ring):
HostWindows.TagAsOverlay(myOverlayHwnd);
```

This ensures all visual-only overlay windows are invisible to every plugin's window enumeration, regardless of which component created them.

---

## 3. Complete Working Example

This is a minimal but complete plugin that appears in **both** the Settings Plugins panel and the Select Action Plugins tab.

### 3.1 manifest.json

```json
{
  "id": "com.yourname.myplugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "A short description of what this plugin does.",
  "author": "Your Name",
  "entryPoint": "MyPlugin.dll",
  "entryClass": "MyPluginNamespace.MyPlugin",
  "icon": "icon.png"
}
```

### 3.2 MyPlugin.cs (Main Plugin Class)

```csharp
using System;
using TouchDial.Sdk;

namespace MyPluginNamespace
{
    public class MyPlugin : IPlugin
    {
        private PluginContext? _context;
        private MyPluginWindow? _activeWindow;

        public void Initialize(PluginContext context)
        {
            _context = context;
            _context.Log("Plugin initialized.");

            // ─── REGISTER ACTIONS ───
            // These appear in: Select Action > Plugins tab
            // Users can assign them to any wheel button.
            
            _context.RegisterAction("toggle_window", "Open My Tool", () =>
            {
                try
                {
                    if (_activeWindow != null)
                    {
                        // Toggle OFF
                        try { _activeWindow.Close(); } catch { }
                        _activeWindow = null;
                    }
                    else
                    {
                        // Toggle ON
                        _activeWindow = new MyPluginWindow();
                        _activeWindow.Closed += (s, e) => { _activeWindow = null; };

                        // Register with host for auto-positioning & Follow Dial.
                        // Use WindowShape.Circular if your window uses a circular GDI region
                        // to get automatic anti-aliased edges from the host.
                        _context.RegisterWindow(_activeWindow);

                        _activeWindow.Activate();
                    }
                }
                catch (Exception ex)
                {
                    _context.Log($"Error: {ex.Message}");
                    _activeWindow = null;
                }
            });

            // You can register multiple actions:
            // _context.RegisterAction("action2", "Do Something Else", () => { ... });
        }

        public void OnEnable() => _context?.Log("Plugin enabled.");
        public void OnDisable() => _context?.Log("Plugin disabled.");

        public void Terminate()
        {
            try { _activeWindow?.Close(); } catch { }
            _activeWindow = null;
            _context?.Log("Plugin terminated.");
        }

        // ─── SETTINGS UI ───
        // This appears in: Settings > Plugins (when user clicks your plugin in the list)
        // Return a WinUI 3 UIElement built in pure C# (no XAML files).
        public object GetSettingsUI()
        {
            return new MyPluginSettingsUI();
        }
    }
}
```

### 3.3 MyPluginWindow.cs (Standalone Tool Window)

```csharp
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using WinRT.Interop;

namespace MyPluginNamespace
{
    public class MyPluginWindow : Window
    {
        public MyPluginWindow()
        {
            Title = "My Plugin Tool";
            Content = BuildUI();

            // Window setup
            var hwnd = WindowNative.GetWindowHandle(this);
            var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
            var appWindow = AppWindow.GetFromWindowId(windowId);

            // Recommended: 530x750 to match Touch Dial's style
            appWindow.Resize(new Windows.Graphics.SizeInt32(530, 750));

            if (appWindow.Presenter is OverlappedPresenter presenter)
            {
                presenter.IsAlwaysOnTop = true;  // Stay above other windows
                presenter.IsResizable = false;
                presenter.IsMaximizable = false;
            }
        }

        private Grid BuildUI()
        {
            var root = new Grid
            {
                Background = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 16, 16, 16)),
                Padding = new Thickness(16)
            };

            root.Children.Add(new TextBlock
            {
                Text = "Hello from My Plugin!",
                Foreground = new SolidColorBrush(Colors.White),
                FontSize = 18,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            });

            return root;
        }
    }
}
```

### 3.4 MyPluginSettingsUI.cs (Settings Panel — shown inline in Settings > Plugins)

```csharp
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;

namespace MyPluginNamespace
{
    /// <summary>
    /// This UI is displayed inside the Settings window when the user clicks
    /// on your plugin in the Plugins list. It appears as an expandable panel
    /// below the plugin entry.
    /// 
    /// IMPORTANT: Build ALL UI in pure C# code. Do NOT use .xaml files.
    /// WinUI 3 cannot resolve XAML resources across dynamically loaded DLL boundaries.
    /// Inherit from Grid, StackPanel, or any UIElement.
    /// </summary>
    public class MyPluginSettingsUI : Grid
    {
        public MyPluginSettingsUI()
        {
            Padding = new Thickness(12);
            RowSpacing = 8;

            RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
            RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

            var label = new TextBlock
            {
                Text = "Plugin settings go here.",
                Foreground = new SolidColorBrush(Colors.White),
                FontSize = 14
            };
            Grid.SetRow(label, 0);
            Children.Add(label);

            var toggle = new ToggleSwitch
            {
                Header = "Enable feature",
                IsOn = true,
                Margin = new Thickness(0, 8, 0, 0)
            };
            Grid.SetRow(toggle, 1);
            Children.Add(toggle);
        }
    }
}
```

---

## 4. Project Setup (.csproj)

Create a **.NET 10 Class Library** project. Here is the complete `.csproj`:

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
    <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
    <UseWinUI>true</UseWinUI>
    <EnableMsixTooling>true</EnableMsixTooling>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <!-- Reference TouchDial.Sdk_v2.0.dll - DO NOT include it in output -->
    <Reference Include="TouchDial.Sdk">
      <HintPath>path\to\TouchDial.Sdk_v2.0.dll</HintPath>
      <Private>false</Private>
    </Reference>
  </ItemGroup>

  <ItemGroup>
    <!-- WinUI 3 SDK - compile only, host provides runtime -->
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260529003">
      <IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```

> **Critical**: The `TouchDial.Sdk_v2.0.dll` must NOT be included in your final `.tdial` package. The host application provides it at runtime. Set `<Private>false</Private>` to prevent it from being copied to your output.

---

## 5. manifest.json Reference

Every plugin **MUST** include a `manifest.json` at the root of the archive:

```json
{
  "id": "com.yourname.pluginname",
  "name": "My Cool Plugin",
  "version": "1.0.0",
  "description": "A short description shown in the Settings plugin list.",
  "author": "Developer Name",
  "entryPoint": "MyPluginAssembly.dll",
  "entryClass": "MyPluginNamespace.MyPlugin",
  "icon": "icon.png"
}
```

| Field | Required | Description |
|---|---|---|
| `id` | Yes | Unique reverse-domain ID. Used as the installation folder name. |
| `name` | Yes | Display name shown in Settings > Plugins list. |
| `version` | Yes | Semantic version string. |
| `description` | Yes | Short description shown below the name in the plugin list. |
| `author` | Yes | Author name. |
| `entryPoint` | Yes | Your compiled DLL filename. |
| `entryClass` | Yes | Full namespace + class name implementing `IPlugin`. |
| `icon` | No | Relative path to a plugin icon (PNG/SVG). Shown in the plugin list. |

---

## 6. Packaging (.tdial)

1. Build your project in **Release** mode.
2. Create this folder structure:
   ```
   (Root)
   ├── manifest.json
   ├── MyPlugin.dll
   ├── icon.png              (optional)
   ├── Dependency.dll         (if any, but NOT TouchDial.Sdk_v2.0.dll)
   └── assets/                (optional)
       └── ...
   ```
3. **ZIP** the contents (not the folder itself).
4. Rename `.zip` to `.tdial`.

### 6.1 Automated Build Script (.csproj PostBuild)

```xml
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
  <PropertyGroup>
    <MyDocs>$([System.Environment]::GetFolderPath(SpecialFolder.MyDocuments))</MyDocs>
    <PluginDest>$(MyDocs)\TouchDial\Plugins\$(AssemblyName)</PluginDest>
  </PropertyGroup>
  
  <!-- Deploy to app directory for testing -->
  <Exec Command="xcopy /Y /I /E /Q &quot;$(TargetDir)*.*&quot; &quot;$(PluginDest)&quot;" />
  <Copy SourceFiles="$(MSBuildProjectDirectory)\manifest.json" DestinationFolder="$(PluginDest)" />
  
  <!-- CRITICAL: Remove SDK DLL to prevent conflicts with Host -->
  <Exec Command="del &quot;$(PluginDest)\TouchDial.Sdk_v2.0.dll&quot; /Q" />

  <!-- Create .tdial archive for distribution -->
  <Exec Command="powershell -Command &quot;try { $src = '$(PluginDest)'; $out = '$(TargetDir)$(AssemblyName).tdial'; $tmp = '$(TargetDir)$(AssemblyName).zip'; Remove-Item $out -ErrorAction SilentlyContinue; Compress-Archive -Path '$src\*' -DestinationPath $tmp -Force; Move-Item $tmp -Destination $out -Force } catch { Write-Error $_.Exception.Message; exit 1 }&quot;" />
</Target>
```

---

## 7. Plugin Lifecycle

The host calls your methods in this order:

```
1. Constructor()        — Object created via Activator.CreateInstance
2. Initialize(context)  — Register actions, setup state. Context is ready.
3. OnEnable()           — Plugin is active
4. ... (user interacts with actions / settings UI) ...
5. OnDisable()          — User disabled the plugin (optional)
6. Terminate()          — App closing or plugin being uninstalled. Clean up!
```

### Important Rules
- **`Initialize`** is where you call `RegisterAction()`. Actions registered here appear in the Select Action > Plugins tab.
- **`GetSettingsUI()`** is called after `Initialize`. The returned control is rendered by the host and embedded **inline** in the Settings > Plugins panel.
- **`Terminate()`** must close any open windows and release resources, otherwise file locks prevent updates.
- All UI must be built in **pure C# code** (no `.xaml` files). XAML resource resolution fails across DLL boundaries in WinUI 3.
- Match the app's **dark theme**: background `#101010`, text `White`, accents as needed.
- Recommended standalone window size: **530×750px**.

---

## 8. Security Constraints

Touch Dial enforces security to prevent plugins from accessing files outside their own directory.

### 8.1 File-System Sandbox

Plugins should use `_context.Sandbox` for all file I/O. The sandbox restricts operations to **your plugin's own installation directory**. Path traversal attempts (e.g. `..\..\config.json`) will throw `UnauthorizedAccessException`.

```csharp
//  Safe — writes inside your plugin folder
_context.Sandbox.WriteText("settings.json", myJson);
string data = _context.Sandbox.ReadText("settings.json");

//  Safe — subdirectories are fine
_context.Sandbox.WriteText("cache/data.bin", content);

//  Blocked — throws UnauthorizedAccessException
_context.Sandbox.WriteText("..\\..\\config.json", malicious);

//  Check before accessing
if (_context.Sandbox.IsPathAllowed("myfile.txt")) { ... }
```

> **Tip**: `_context.InstallationPath` still gives you the absolute path for cases where you need to pass it to third-party libraries, but prefer `Sandbox` for your own read/write operations.

---

## 9. Localization / Language Support

Plugins can read the current language and react to language changes in real-time via `PluginContext`:

- **`_context.CurrentLanguage`** — returns the active language display name (e.g. `"English"`, `"Deutsch"`).
- **`_context.LanguageChanged`** — event fired immediately when the user changes the language in Settings (no need to wait for Apply).

### Supported Language Values

| Display Name | Code |
|---|---|
| `"English"` | en |
| `"Deutsch"` | de |
| `"Français"` | fr |
| `"Español"` | es |
| `"Italiano"` | it |
| `"Português"` | pt |
| `"中文"` | zh |
| `"हिन्दी"` | hi |

### Example: Localizing a Plugin

```csharp
public class MyPlugin : IPlugin
{
    private PluginContext? _context;
    private TextBlock? _label;

    // Define translations keyed by the display name from PluginContext.CurrentLanguage
    private static readonly Dictionary<string, string> _greeting = new()
    {
        ["English"]   = "Hello!",
        ["Deutsch"]   = "Hallo!",
        ["Français"]  = "Bonjour !",
        ["Español"]   = "¡Hola!",
        ["Italiano"]  = "Ciao!",
        ["Português"] = "Olá!",
        ["中文"]       = "你好！",
        ["हिन्दी"]      = "नमस्ते!",
    };

    public void Initialize(PluginContext context)
    {
        _context = context;

        // Subscribe to live language changes
        _context.LanguageChanged += OnLanguageChanged;
    }

    private void OnLanguageChanged(string newLanguage)
    {
        // Update UI text whenever the user switches language
        if (_label != null)
            _label.Text = GetTranslation(_greeting, newLanguage);
    }

    private static string GetTranslation(Dictionary<string, string> dict, string lang)
    {
        return dict.TryGetValue(lang, out var val) ? val : dict["English"];
    }

    public object GetSettingsUI()
    {
        _label = new TextBlock
        {
            Text = GetTranslation(_greeting, _context?.CurrentLanguage ?? "English"),
            Foreground = new SolidColorBrush(Microsoft.UI.Colors.White),
            FontSize = 14
        };
        return _label;
    }

    // ... OnEnable, OnDisable, Terminate as usual
}
```

> **Tip**: Always provide an `"English"` fallback in your dictionaries — it is the default language and will be used if a key is missing.

---

## 9.1 Outer Ring Color Matching

Plugins can read the current Outer Ring color and react to color changes in real-time via `PluginContext`. This allows plugins to match their borders or theme to the main dial's appearance — including rainbow animation.

- **`_context.OuterRingR / G / B`** — returns the current Outer Ring color components (0-255), with brightness already applied.
- **`_context.OuterRingColorChanged`** — event fired whenever the color changes (Settings change or rainbow animation tick at ~20fps).

### Example: Matching a Plugin Border to the Outer Ring

```csharp
public class MyPlugin : IPlugin
{
    private PluginContext? _context;
    private Border? _border;

    public void Initialize(PluginContext context)
    {
        _context = context;

        // Subscribe to live color changes (including rainbow animation)
        _context.OuterRingColorChanged += OnOuterRingColorChanged;
    }

    private void OnOuterRingColorChanged(byte r, byte g, byte b)
    {
        // Update border color to match the dial's Outer Ring
        if (_border != null)
        {
            _border.BorderBrush = new SolidColorBrush(
                Windows.UI.Color.FromArgb(255, r, g, b));
        }
    }

    public object GetSettingsUI()
    {
        // Read the initial color
        byte r = _context!.OuterRingR;
        byte g = _context.OuterRingG;
        byte b = _context.OuterRingB;

        _border = new Border
        {
            BorderBrush = new SolidColorBrush(
                Windows.UI.Color.FromArgb(255, r, g, b)),
            BorderThickness = new Thickness(2),
            Child = new TextBlock { Text = "Themed content" }
        };
        return _border;
    }

    // ... OnEnable, OnDisable, Terminate as usual
}
```

> **Note**: The color values include the user's brightness adjustment. During rainbow mode, the event fires at ~20fps with smoothly cycling colors.

---

## 10. Wheel Geometry & Dimensions Reference

These are the exact sizes (in **DIPs** unless noted) of the Touch Dial wheel UI. Use them if your plugin needs to overlay, surround, or position elements relative to the wheel.

### 10.1 Main Wheel (Expanded State)

| Constant | Value | Unit | Description |
|---|---|---|---|
| Window size | **320 × 320** | px (hardware) | Total window footprint |
| Wheel outer ring diameter | **250** | DIPs | Main visible ring outer edge |
| Wheel outer ring stroke | **3** | DIPs | Thickness of the outer ring border |
| Center shell diameter | **72** | DIPs | Inner hub / center button area |
| Center size (open) | **69** | DIPs | Clickable center region |
| Max button slices | **9** | — | 8 action slots + 1 settings slot |
| Slice layout | 360° ÷ slice count | — | Evenly divided, clockwise from top |

### 10.2 Collapsed / Closed State

| Constant | Value | Unit | Description |
|---|---|---|---|
| Closed ring diameter | **82** | DIPs | Visible ring when collapsed |
| Closed ring stroke | **3** | DIPs | Stroke thickness when collapsed |
| Closed hit-test region | **79** | px | Win32 click region diameter |

### 10.3 Folder Menu (Expanded)

When a folder is opened, a **separate overlay window** appears centered on the wheel:

| Constant | Value | Unit | Description |
|---|---|---|---|
| Menu diameter | **520** | DIPs | Outer diameter of the folder ring |
| Wheel slice count | **12** | — | Default number of folder item slots |
| Content radial shift | **6** | DIPs | Items pushed outward from ring center |
| Outline stroke thickness | **2** | DIPs | Ring outline border |

### 10.4 Positioning Notes

- The main wheel window is **frameless and transparent**. `GetMainWindowBounds()` returns its screen-coordinate bounding rect `(X, Y, Width, Height)`.
- To center an overlay on the wheel: `centerX = X + Width / 2`, `centerY = Y + Height / 2`.
- `GetMainWindowBounds` is a **snapshot delegate** — poll it in a timer to track wheel movement (e.g., if the user has "Follow Dial" enabled).
- `GetMainWindowHandle` returns the **native HWND** of the wheel window. Use this to create **child windows** (`WS_CHILD`) that move with the dial automatically at the OS level — no polling, zero lag. This is the preferred approach for overlays that must stay perfectly locked to the dial (e.g., visual effects on the center button).
- `RegisterWindow()` positions your window **beside** the wheel (with a 20 DIP gap). If you want to **overlay on top** of the wheel, position the window yourself instead of using `RegisterWindow()`.
- `RegisterWindow(window, WindowShape.Circular)` additionally creates an **anti-aliased ring overlay** around the circular GDI region boundary. This eliminates jagged pixel edges on circular windows without any extra work from the plugin.
- The folder menu is a separate window **centered on the wheel**. If your plugin surrounds the wheel, consider detecting when the folder menu is open (the wheel bounds remain 320×320 even when the folder is visible).

#### Child Window Overlay Pattern

For overlays that must be locked to the dial (zero drag lag), create a Win32 **layered child window**:

```csharp
// Get the wheel HWND from the SDK
IntPtr wheelHwnd = context.GetMainWindowHandle?.Invoke() ?? IntPtr.Zero;

// Create a layered, click-through child window
uint exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT;
IntPtr overlay = CreateWindowExW(exStyle, "Static", "MyOverlay",
    WS_CHILD, relX, relY, width, height,
    wheelHwnd, IntPtr.Zero, hInstance, IntPtr.Zero);

// Update content with per-pixel alpha (position via SetWindowPos, content via UpdateLayeredWindow)
SetWindowPos(overlay, IntPtr.Zero, relX, relY, w, h, SWP_NOZORDER | SWP_NOACTIVATE);
UpdateLayeredWindow(overlay, screenDc, IntPtr.Zero, ref size, memDc, ref ptSrc, 0, ref blend, ULW_ALPHA);
```

Key points:
- **`WS_CHILD`** — the overlay moves with the parent (wheel) automatically
- **`WS_EX_LAYERED`** — enables per-pixel alpha transparency (supported on child windows since Windows 8)
- **`WS_EX_TRANSPARENT`** — click-through; pointer events pass to the wheel underneath
- Position with `SetWindowPos` using **parent-relative** coordinates
- Update content with `UpdateLayeredWindow` passing `NULL` for `pptDst` (content only, no position change)
- No need for `EnsureTopmost` or position polling — the OS handles it

### 10.5 Visual Reference (Cross-Section)

```
         ◄──────── 520 DIP (folder menu) ────────►
         ┌─────────────────────────────────────────┐
         │             Folder Ring                  │
         │    ◄──── 320 px (window) ────►           │
         │    ┌─────────────────────────┐           │
         │    │  ◄── 250 DIP (ring) ──► │           │
         │    │  ┌───────────────────┐  │           │
         │    │  │   ◄ 72 DIP hub ►  │  │           │
         │    │  │   ┌───────────┐   │  │           │
         │    │  │   │  Center   │   │  │           │
         │    │  │   └───────────┘   │  │           │
         │    │  └───────────────────┘  │           │
         │    └─────────────────────────┘           │
         └─────────────────────────────────────────┘
```

---

## 11. Testing

1. Open Touch Dial Settings.
2. Scroll to the **Plugins** section.
3. Click **"Install Plugin"**.
4. Select your `.tdial` file.
5. Verify:
   - Your plugin appears in the **Settings > Plugins** list **immediately — no app restart required**.
   - The row shows your name, description, and icon.
   - Clicking it expands your settings UI panel inline below the list (your `ISettingsProvider`
     schema or your `GetSettingsUI()` control — see §1b).
   - In the **Edit Button > Action** menu, under the **Plugins** tab, your registered action(s)
     appear. A plugin lists **once** (settings are not a separate action — they live only under
     Settings > Plugins). A single-action plugin shows just its plugin name.
   - Assigning an action to a wheel button and pressing it triggers your handler. If it opens a
     window, it appears in front of other windows, follows the dial, and toggles closed on the next
     press.
6. To remove or disable plugins, click **"Manage Plugins"**: third-party plugins can be **deleted**
   (removed from disk — the host releases the DLL lock automatically, no restart), and built-in
   plugins can be **enabled/disabled**.
7. Check `Documents\TouchDial\Plugins\{id}\plugin.log` for your log messages.

---

## 12. Troubleshooting

### Updating / removing a plugin while the app is running
The host (`TouchDial.PluginHost.exe`) loads your DLL, so the file is locked while running. The host
handles this for you:
- **Uninstall while running** — "Manage Plugins" → delete works live: the host unloads your plugin
  (releasing the DLL lock via a collectible load context), deletes the folder, and refreshes the list.
  No restart needed in the common case.
- **Reinstall / update** — if a file is still locked, the host falls back to **Safe Replacement**:
  the old folder is renamed, the update is staged, and stale folders are cleaned up on next launch.

**Tip**: Always clean up windows, timers, and handles in `Terminate()` to minimize file locks.


---

## 13. Want an AI to build a plugin for you?

These are the Files You Need to Give an AI:

1. **This template file** (Plugin-Development-Template_Release_v2.0.md)
2. **`TouchDial.Sdk_v2.0.dll`** — the compiled SDK assembly (The TouchDial.Sdk file provided)
3. **Your instructions** — describe what the plugin should do, what actions to register, what UI to show

The AI will need to:
- Create a .NET 10 Class Library project
- Reference `TouchDial.Sdk_v2.0.dll` (with `<Private>false</Private>`)
- Implement `IPlugin` with all 5 methods
- Register actions in `Initialize()` for the Action Picker
- Return a C#-built UIElement from `GetSettingsUI()` for the Settings panel
- Create a `manifest.json`
- Package as `.tdial`
