# Strudel Nostr Save & Share — Architecture Plan

## Overview

Add the ability to save Strudel songs, instruments, and sound/sample references to Nostr, and to browse/discover content shared by others. All content is **public** (no encryption). Users can save their own creations, browse others' shared content, and load it into the editor.

## Content Types

### 1. Song
A complete Strudel program — the full editor content. This is what you'd press Play on.

```javascript
// Example song content
const bass = note("c1 [~ c1] [~ c1] [~ <c1 g1>]")
  .s("sine").gain(0.7).cutoff(300).shape(0.2);

const melody = note("<c4 e4 g4 a4>(3,8)")
  .s("sawtooth").room(0.5).lpf(2000);

stack(bass, melody).scope().punchcard()
```

### 2. Instrument
A reusable code snippet that defines a sound/pattern. Independent — you paste it into the editor and use it.

```javascript
// Example instrument
const bass = note("c1 [~ c1] [~ c1] [~ <c1 g1>]")
  .s("sine").gain(0.7).cutoff(300).shape(0.2);
```

### 3. Sound/Sample Pack Reference
A reference to an external sample pack that can be loaded via Strudel's `samples()` function. This is a URL pointing to a JSON manifest or a GitHub repo shorthand.

```javascript
// Example sound references
samples('github:tidalcycles/dirt-samples')
samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples')
samples('https://example.com/my-samples.json')
```

## Nostr Event Schema

### Kind Selection: Custom Addressable Kinds

We use **custom addressable event kinds** in the `30000-39999` range. These are **parameterized replaceable events** — each event is uniquely identified by `kind` + `pubkey` + `d` tag, meaning you can update/replace a song or instrument in place without creating duplicates.

**Verified unused** (queried across `wss://relay.damus.io`, `wss://nos.lol`, `wss://relay.primal.net`, `wss://nostr.wine`, `wss://purplepag.es` — zero results):

| Kind | Content Type | Description |
|------|-------------|-------------|
| `32220` | Instrument / Code Snippet | A reusable Strudel instrument or pattern snippet |
| `32221` | Song | A complete Strudel composition |
| `32222` | Sound / Sample Pack | A reference to a sample pack URL for use with `samples()` |

Using dedicated kinds means:
- Other clients can easily discover and filter Strudel content by kind
- No namespace collisions with other apps
- Semantically clear — kind `32221` unambiguously means "Strudel song"
- Still fully addressable/replaceable via `d` tag

### Event Structure

#### Song Event (kind 32221)

```javascript
{
  kind: 32221,
  created_at: Math.floor(Date.now() / 1000),
  tags: [
    ["d", "<unique-id>"],                      // Unique identifier (timestamp-based)
    ["title", "My Awesome Track"],             // Human-readable title
    ["t", "ambient"],                          // Hashtag (optional, multiple allowed)
    ["t", "generative"],
    ["description", "A chill ambient piece"],  // Optional description
    ["alt", "Strudel song: My Awesome Track"], // NIP-31 fallback for unknown-kind clients
    ["client", "strudel-ndk"]                  // Client identifier
  ],
  content: "// Full Strudel code here\nnote(\"c a f e\").s(\"sawtooth\").room(0.5)"
}
```

#### Instrument Event (kind 32220)

```javascript
{
  kind: 32220,
  created_at: Math.floor(Date.now() / 1000),
  tags: [
    ["d", "<unique-id>"],
    ["title", "Sub Bass"],
    ["t", "bass"],
    ["t", "synth"],
    ["description", "Deep sub bass with sine wave"],
    ["alt", "Strudel instrument: Sub Bass"],
    ["client", "strudel-ndk"]
  ],
  content: "const bass = note(\"c1 [~ c1] [~ c1] [~ <c1 g1>]\")\n  .s(\"sine\")\n  .gain(0.7)\n  .cutoff(300)\n  .shape(0.2);"
}
```

#### Sound/Sample Pack Reference Event (kind 32222)

```javascript
{
  kind: 32222,
  created_at: Math.floor(Date.now() / 1000),
  tags: [
    ["d", "<unique-id>"],
    ["title", "Dirt Samples"],
    ["t", "drums"],
    ["t", "percussion"],
    ["description", "Classic TidalCycles dirt samples"],
    ["r", "github:tidalcycles/dirt-samples"],  // The samples() URL/reference
    ["alt", "Strudel sample pack: Dirt Samples"],
    ["client", "strudel-ndk"]
  ],
  content: "samples('github:tidalcycles/dirt-samples')"
}
```

### Tag Design Rationale

- **`d` tag**: Unique identifier (timestamp or random string) — makes the event addressable and replaceable.
- **`title` tag**: Human-readable name for display in lists.
- **`t` tag**: Standard hashtags for discovery/search (NIP-01).
- **`description` tag**: Optional longer description.
- **`r` tag**: For sound events, the actual samples URL/reference (NIP-24 standard).
- **`alt` tag**: NIP-31 fallback text for clients that don't know these kinds.
- **`client` tag**: Identifies our app (NIP-89 convention).

### Querying

```javascript
// My songs
{ kinds: [32221], authors: [myPubkey] }

// My instruments
{ kinds: [32220], authors: [myPubkey] }

// My sounds
{ kinds: [32222], authors: [myPubkey] }

// All my strudel content
{ kinds: [32220, 32221, 32222], authors: [myPubkey] }

// Browse others' songs (no author filter)
{ kinds: [32221], limit: 50 }

// Browse by hashtag
{ kinds: [32220, 32221, 32222], '#t': ['ambient'], limit: 50 }
```

## Data Flow

```mermaid
graph TD
    A[Strudel Editor] -->|text selected| B{Selection?}
    B -->|yes| C[Save Instrument enabled]
    B -->|no| D[Save Instrument dimmed]
    
    A -->|Save Song| E[Save Dialog in Sidebar]
    C -->|Save Instrument| E
    E -->|title + tags| F[Build Nostr Event]
    F -->|publishEvent| G[NDK Worker]
    G -->|signed event| H[Relays]
    
    I[Sidebar Songs Box] -->|subscribe| G
    G -->|ndkEvent| I
    I -->|click song| J[Replace editor content]
    
    K[Sidebar Instruments Box] -->|subscribe| G
    G -->|ndkEvent| K
    K -->|click instrument| L{Cursor/Selection?}
    L -->|text selected| M[Replace selection]
    L -->|cursor only| N[Insert at cursor]
```

## UI Design

### Design Principles

All save/load/browse functionality lives in the **sidebar** to keep the main editor area clean. No new buttons in the control panel. The sidebar is the single interface for managing songs and instruments.

### Sidenav Layout

The existing sidenav `divSideNavBody` area (currently has an empty `divFiles` div) will be repurposed to show two boxes: Songs and Instruments.

```
divSideNav
├── divMIDISection (existing)
├── divAISection (existing)
├── divSongsSection ← NEW
│   ├── divSongsSectionTitle: Songs
│   ├── divSongsSaveRow
│   │   ├── input: title
│   │   └── button: Save Song (always enabled)
│   └── divSongsList (scrollable list)
│       └── per item: title, click-to-load, delete button
├── divInstrumentsSection ← NEW
│   ├── divInstrumentsSectionTitle: Instruments
│   ├── divInstrumentsSaveRow
│   │   ├── input: title
│   │   └── button: Save Instrument (dimmed when no selection)
│   └── divInstrumentsList (scrollable list)
│       └── per item: title, click-to-load, delete button
├── divSideNavBody (existing)
├── divRelaySection (existing)
└── divVersionBar (existing)
```

### Songs Box Behavior

```
┌─────────────────────────────────────┐
│  Songs                              │
│  ┌──────────────────────┐ ┌──────┐  │
│  │ Title...             │ │ Save │  │
│  └──────────────────────┘ └──────┘  │
│  ┌─────────────────────────────────┐│
│  │ 🎵 My Ambient Track        [×] ││
│  │ 🎵 Drum & Bass Loop        [×] ││
│  │ 🎵 Generative Piece        [×] ││
│  └─────────────────────────────────┘│
└─────────────────────────────────────┘
```

- **Save Song**: Always enabled. Saves the **entire editor content** as a song.
- **Click a song**: **Replaces** the entire editor content with the song's code.
- **[×] button**: Deletes the song from Nostr (kind 5 deletion request).

### Instruments Box Behavior

```
┌─────────────────────────────────────┐
│  Instruments                        │
│  ┌──────────────────────┐ ┌──────┐  │
│  │ Title...             │ │ Save │  │
│  └──────────────────────┘ └──────┘  │
│  ┌─────────────────────────────────┐│
│  │ 🎹 Sub Bass                [×] ││
│  │ 🎹 Arp Melody              [×] ││
│  │ 🎹 Hi-Hat Pattern          [×] ││
│  └─────────────────────────────────┘│
└─────────────────────────────────────┘
```

- **Save Instrument**: **Dimmed/disabled** when no text is selected in the editor. When text IS selected, saves the **selected text** as an instrument.
- **Click an instrument**:
  - If text is **selected** in the editor → **replaces the selection** with the instrument code
  - If **no selection** (just a cursor) → **inserts** the instrument code at the cursor position
- **[×] button**: Deletes the instrument from Nostr.

### Selection-Aware State Machine

```mermaid
stateDiagram-v2
    state Editor {
        [*] --> NoSelection
        NoSelection --> TextSelected: user selects text
        TextSelected --> NoSelection: user deselects
    }
    
    state SaveInstrumentButton {
        Dimmed --> Enabled: text selected
        Enabled --> Dimmed: text deselected
    }
    
    state LoadInstrument {
        [*] --> CheckSelection
        CheckSelection --> InsertAtCursor: no selection
        CheckSelection --> ReplaceSelection: text selected
    }
```

### Future: Browse Community (Phase 2)

A "Browse" toggle or tab within each box to switch between "My Items" and "Community" views. This keeps everything in the sidebar without adding modals.

```
┌─────────────────────────────────────┐
│  Songs  [My] [Community]            │
│  ┌─────────────────────────────────┐│
│  │ 🎵 Cool Track — npub1ab... [+] ││
│  │ 🎵 Ambient — npub1cd...    [+] ││
│  └─────────────────────────────────┘│
└─────────────────────────────────────┘
```

## File Structure

### New File: `www/js/strudel-nostr.mjs`

The main module for all Nostr save/load/browse functionality.

```javascript
// Constants
const KINDS = { INSTRUMENT: 32220, SONG: 32221, SOUND: 32222 };

// Exports:
saveSong(title, code, tags, description)       // Publish kind 32221 event
saveInstrument(title, code, tags, description) // Publish kind 32220 event
deleteItem(kind, dTag)                         // Publish kind 5 deletion event
subscribeToMyItems(pubkey, kind, callback)     // Subscribe to user's items
subscribeToCommunity(kind, limit, callback)    // Subscribe to community items
buildEvent(kind, title, code, tags, desc)      // Build unsigned event object
parseStrudelEvent(event)                       // Parse event into display object
getEditorSelection(editor)                     // Get selected text from editor
insertAtCursor(editor, code)                   // Insert code at cursor position
replaceSelection(editor, code)                 // Replace selected text with code
replaceEditorContent(editor, code)             // Replace entire editor content
```

### Modified Files

- **`www/strudel.html`** — Add Songs/Instruments sections to sidenav, import strudel-nostr.mjs, wire up event handlers, track editor selection state
- **`www/css/strudel.css`** — Add styles for songs/instruments sidebar sections

## Implementation Phases

### Phase 1: Core Save/Load (Sidebar)
- Create `strudel-nostr.mjs` with save/load/editor-interaction functions
- Add Songs section to sidenav with save row and list
- Add Instruments section to sidenav with save row and list
- Track editor selection state to enable/disable Save Instrument button
- Implement Save Song (entire editor content → kind 32221)
- Implement Save Instrument (selected text → kind 32220)
- Subscribe to user's own songs/instruments on page load
- Click-to-load: songs replace editor, instruments insert/replace at cursor
- Delete from library (kind 5 deletion)

### Phase 2: Browse Community
- Add My/Community toggle to each section
- Subscribe to community events (no author filter)
- Display author npub in community items
- Load community content into editor

### Phase 3: Share via Link
- Generate naddr strings for saved items
- Copy-to-clipboard functionality
- Paste/load from naddr input

## Technical Considerations

### Strudel Code as Content
- Strudel code is plain JavaScript — no special encoding needed
- The `content` field holds the raw code string
- For instruments, the code is the selected snippet
- For songs, the code is the complete editor content

### Editor Interaction via CodeMirror
The strudel-editor component wraps a CodeMirror 6 editor. To interact with selections and cursor:
- `editor.code` — get/set the full editor content
- CodeMirror's `EditorView` provides selection state via `view.state.selection`
- `view.dispatch()` can insert/replace text at specific positions
- We need to access the underlying CodeMirror view from the strudel-editor component

### Loading Instruments
When clicking an instrument in the sidebar:
- If text is selected → replace the selection with the instrument code
- If no selection → insert at cursor position
- The user can then reference the defined variable in their song

### Loading Songs
When clicking a song in the sidebar:
- Replace the entire editor content with the song's code
- No confirmation dialog for now (keep it simple)

### Event Size Limits
Relay event size limits are typically 64KB-128KB. Strudel code is text-based and compact, so this should not be an issue. A typical song is well under 1KB.

### Replaceable Events
Using custom addressable kinds (32220, 32221, 32222) with `d` tags means updating a song or instrument (same d-tag) replaces the old version on relays. Each save with a new title/id creates a new event.
