# Analyzer settings

Declare what your analyzer can be configured with; the host draws the pane.

## Overview

As of SDK 3.0.0 an analyzer's settings are **data, not a view**. Your principal class returns an `AnalyzerSettingsConfig` and the host renders **Settings ▸ Analyzers ▸ *your analyzer*** from it: the coloring row, one switch per option, and the editable Name/Ports/Source table — all matching the built-in analyzers pixel for pixel.

> Important: `settingsView(_:)` is no longer called. A plugin that only implements `settingsView(_:)` still loads and analyzes packets correctly, but its settings pane reads "No configurable options". Return an `AnalyzerSettingsConfig` to appear configurable.

## The short version

Conform to ``CPAPluginSettingsHosting`` and declare three things:

```swift
@MainActor
final class NTPPlugInController: NSObject, ObservableObject,
    CPAPluginPrincipalClassProtocol,
    CPAPluginSettingsHosting {

    nonisolated private static let prefix = "com_example_cpa_ntp"
    nonisolated static let kPortsKey = "\(prefix)_protocols"
    nonisolated static let kLocalTimestamps = "\(prefix)_local_timestamps"

    // Port edits re-register through this, so it must be stored and settable.
    @Published var pluginStruct: CPAPlugInStructure?

    var settingsColor: AnalyzerColorSpec? { .prefixed(prefix) }

    var settingsToggles: [AnalyzerToggleSpec] {
        [AnalyzerToggleSpec(
            defaultsKey: Self.kLocalTimestamps,
            labelKey: "Show NTP timestamps in local time",
            defaultValue: false
        )]
    }

    var settingsPorts: AnalyzerPortsSpec? {
        .rows([.udp("NTP", [123])], defaultsKey: Self.kPortsKey)
    }
}
```

That is the whole settings implementation. ``CPAPluginSettingsHosting`` supplies `analyzerSettingsConfig`, `usesColor()`, `protocolColor()`, `preferencesSheet()`, `setDefaultSettings(reset:)` and the three `CPAPortsPreferencesProtocol` port-mutation methods the port editor calls when the user edits a row.

Conforming to ``CPAPluginSettingsHosting`` replaces conforming to `CPAPluginPreferencesProtocol` — it refines it, so you get both.

## The three declarations

### Coloring

``AnalyzerColorSpec`` names two UserDefaults keys: a `Bool` for the switch and a `String` for the swatch. ``AnalyzerColorSpec/prefixed(_:)`` derives both from one prefix.

The swatch is stored as `Color.rawValue` — a base64 archived `NSColor`. Read and write it through ``CPAAnalyzerColorStore``, never `NSKeyedUnarchiver` directly: the host's color picker writes `Color.rawValue`, so a plugin that archives an `NSColor` itself will never see the user's pick. The default `protocolColor()` already does the right thing.

### Toggles

Each ``AnalyzerToggleSpec`` is one switch bound to a `Bool` UserDefaults key. `labelKey` is the visible label. The host localizes keys it already ships (`"Compute checksums"`, `"Keep usernames and passwords private"`, …) and gives them a matching SF Symbol and a canonical position; any other key is displayed **verbatim**, with a generic symbol, after the known ones. So write `labelKey` as the English sentence you want on screen.

Read the value back wherever the option applies — including from your analyzer, on a background thread:

```swift
private static var usesLocalTimestamps: Bool {
    UserDefaults.standard.bool(forKey: NTPPlugInController.kLocalTimestamps)
}
```

`UserDefaults` reads are thread-safe, and the host posts `CPAProtocolKonstants.kClearAnalyzingCaches` when a setting changes, so already-analyzed packets are re-dissected with the new value. Declare the key `nonisolated` if it lives on your `@MainActor` principal class.

### Ports

``AnalyzerPortsSpec`` drives the editable table. Build it from rows:

```swift
.rows(
    [.udp("NTP", [123]), .tcp("NTS-KE", [4460])],
    names: ["NTP", "SNTP", "NTS-KE"],
    defaultsKey: Self.kPortsKey
)
```

- Each row is a table line and a factory default that **Reset** restores.
- `names` populates each row's Name menu. Omit it and the row names are the only choices.
- `defaultsKey` is where the user's edited table is persisted. Namespace it per plugin.

Return `nil` for analyzers that are not port-based; the host then shows the "not port-based" note instead of an empty table.

## Registering the user's ports, not yours

This is the one thing easy to get wrong. If you register hard-coded ports, every launch re-binds the factory ports and the user's edits are silently discarded. Seed defaults first, then register what is actually stored:

```swift
init?(controller: CPAPlugInsController) {
    super.init()
    setDefaultSettings()          // seeds every declared key, if absent
    register(with: controller)
}

private func register(with controller: CPAPlugInsController) {
    let plugin = CPAPlugInStructure.register(
        // …
        targets: configuredTargetProtocols,   // ← the stored rows
        columns: columns
    )
    _ = controller.register(CPAPlugIn: plugin, forBundle: pluginBundle)
    pluginStruct = plugin                     // ← the port editor mutates this
}
```

``CPAPluginSettingsHosting/configuredTargetProtocols`` reads the persisted table and falls back to your declared defaults. Assigning `pluginStruct` afterwards is what lets the port editor re-register on an edit — forget it and port changes throw `CPAError.protocolCreationFailure`.

## Custom UI

``CPAPluginSettingsHosting/settingsExtraContent`` appends an arbitrary `AnyView` below the switches. Prefer toggles: the pane reserves a fixed height for the options block so every analyzer's Ports section starts at the same Y, and a tall custom view breaks that alignment. It exists for genuinely bespoke surfaces (the built-in TLS analyzer's key-log section is the only in-tree use).

## Topics

### Declaring settings

- ``CPAPluginSettingsHosting``
- ``AnalyzerSettingsConfig``
- ``AnalyzerColorSpec``
- ``AnalyzerToggleSpec``
- ``AnalyzerPortsSpec``

### Storage

- ``CPAAnalyzerColorStore``
- ``AnalyzerPortsStore``
