termflix 0.7.2

Terminal animation player with 60 procedurally generated animations, multiple render modes, and true color support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# External Visual Control

Real-time animation control for termflix via stdin piping or a watched file using a lightweight ndjson protocol.

## Table of Contents

- [Overview]#overview
- [Input Sources]#input-sources
  - [Stdin Mode]#stdin-mode
  - [File Watch Mode]#file-watch-mode
- [Protocol Reference]#protocol-reference
  - [Message Format]#message-format
  - [Field Reference]#field-reference
  - [Render Mode Values]#render-mode-values
  - [Color Mode Values]#color-mode-values
- [Architecture]#architecture
  - [Data Flow]#data-flow
  - [Frame Integration]#frame-integration
  - [State Merging]#state-merging
- [Animation Trait Extension]#animation-trait-extension
  - [Default Behavior]#default-behavior
  - [Semantic Overrides]#semantic-overrides
- [Configuration]#configuration
- [Usage Examples]#usage-examples
  - [Stdin Basics]#stdin-basics
  - [File-Based Control]#file-based-control
  - [Continuous Control Loops]#continuous-control-loops
  - [Partial Updates]#partial-updates
- [Keyboard Shortcuts]#keyboard-shortcuts
- [Error Handling]#error-handling
- [Source Files]#source-files
- [Related Documentation]#related-documentation

---

## Overview

**Purpose:** Allow external processes — scripts, daemons, other programs — to drive termflix animations in real time without modifying the player itself.

**When to use:** Any situation where animation state needs to respond to events outside the terminal: music beat detection, system monitoring, CI/CD status boards, interactive installations, or scripted demonstrations.

**How it works:**

1. An external process writes ndjson messages to stdin or a watched file.
2. A background thread in termflix reads and parses each line.
3. Messages are forwarded via an mpsc channel to the main render loop.
4. The main loop drains the channel on every frame and merges the latest state.
5. Effects are applied during post-processing before the frame is written to the terminal.

> **📝 Note:** When no stdin pipe and no `--data-file` are configured, termflix behavior is identical to normal operation. The feature has zero overhead when idle because the channel `try_recv()` call is non-blocking.

---

## Input Sources

The following diagram shows how the two input sources map to internal components.

```mermaid
graph TD
    Pipe["stdin pipe\n(process | termflix)"]
    File["watched file\n(--data-file PATH)"]
    TTYCheck["is_terminal() check\nin src/main.rs"]
    BgThread["Background thread\nsrc/external.rs"]
    Channel["mpsc::channel\nExternalParams"]
    MainLoop["Main render loop\nsrc/main.rs"]

    Pipe --> TTYCheck
    TTYCheck -->|"stdin is NOT a TTY"| BgThread
    File -->|"--data-file flag\nor config data_file"| BgThread
    BgThread --> Channel
    Channel -->|"try_recv() each frame"| MainLoop

    style Pipe fill:#4a148c,stroke:#9c27b0,stroke-width:2px,color:#ffffff
    style File fill:#4a148c,stroke:#9c27b0,stroke-width:2px,color:#ffffff
    style TTYCheck fill:#37474f,stroke:#78909c,stroke-width:2px,color:#ffffff
    style BgThread fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
    style Channel fill:#880e4f,stroke:#c2185b,stroke-width:2px,color:#ffffff
    style MainLoop fill:#e65100,stroke:#ff9800,stroke-width:3px,color:#ffffff
```

### Stdin Mode

termflix automatically activates stdin mode when its standard input is not a TTY. This is detected via `is_terminal()` — no flag is required.

```bash
# Pipe a command — stdin is no longer a TTY, so external control activates
echo '{"animation":"plasma","speed":2.0}' | termflix
```

> **⚠️ Warning:** You cannot use stdin mode while also typing interactively, because stdin is consumed by the reader thread. Use `--data-file` when you need both interactive keyboard control and external updates.

### File Watch Mode

Activate file watching with the `--data-file PATH` flag or the `data_file` config key. The file watcher uses OS-native change notifications (via the `notify` crate) rather than polling.

**Startup behavior:** If the file already exists when termflix starts, the last non-empty line is read immediately. This lets you set an initial state before launching.

**On change:** When the file is modified, termflix reads the last non-empty line from the file. Writing multiple lines to the file is supported; only the last line is used.

```bash
# Start with an initial state already in the file
echo '{"animation":"fire","intensity":0.7}' > /tmp/tf.json
termflix --data-file /tmp/tf.json
```

> **✅ Tip:** File watch mode is ideal for integration with external tools that cannot pipe to stdin, such as cron jobs, systemd services, or GUI applications writing to a shared path.

---

## Protocol Reference

### Message Format

The protocol is **ndjson** (newline-delimited JSON): one JSON object per line. All fields are optional — include only what you want to change.

```json
{ "animation": "fire", "speed": 1.5, "intensity": 0.8, "color_shift": 0.25 }
```

Lines that are empty, whitespace-only, or contain invalid JSON are silently skipped. The player never crashes on bad input.

> **📝 Note:** Each message is a **delta**, not a full replacement. Fields you omit retain their current values. Send `{"speed": 2.0}` to double the speed while keeping the current animation, intensity, and everything else unchanged.

### Field Reference

| Field | Type | Range | Effect |
|-------|------|-------|--------|
| `animation` | string | any valid name | Switch to the named animation |
| `speed` | number | 0.1 – 5.0 | Virtual time multiplier (does not change FPS cap) |
| `intensity` | number | 0.0 – 2.0 | Canvas brightness scale applied as post-processing |
| `color_shift` | number | 0.0 – 1.0 | Hue rotation fraction applied to all pixels |
| `scale` | number | 0.5 – 2.0 | Rebuild animation with a new particle/element scale |
| `render` | string | see below | Change render mode |
| `color` | string | see below | Change color mode |

**speed** controls how fast virtual time advances, not how many frames per second are drawn. At `speed: 2.0` the animation logic sees twice the elapsed time per frame, making it run twice as fast while the FPS cap is unchanged.

**intensity** is applied by multiplying every pixel brightness value after the animation draws. Values above `1.0` push pixels toward maximum brightness. Values below `1.0` dim the canvas.

**color_shift** performs an RGB → HSV conversion, rotates the hue by `color_shift * 360°`, then converts back to RGB. A value of `0.5` rotates all colors by 180° (approximate complementary colors). A value of `1.0` completes a full cycle back to the original.

**scale** causes the animation to be fully rebuilt with a new particle or element count. This is more expensive than other fields because it reallocates internal animation state.

### Render Mode Values

| Value | Description |
|-------|-------------|
| `braille` | Braille dot characters (highest resolution) |
| `half-block` | Unicode half-block characters (default) |
| `ascii` | ASCII characters only |

> **📝 Note:** `halfblock` (no hyphen) is also accepted as an alias for `half-block`.

### Color Mode Values

| Value | Description |
|-------|-------------|
| `mono` | Single color (white/black only) |
| `ansi16` | 16-color ANSI palette |
| `ansi256` | 256-color ANSI palette |
| `true-color` | 24-bit RGB (default) |

> **📝 Note:** `truecolor` (no hyphen) is also accepted as an alias for `true-color`.

---

## Architecture

### Data Flow

The following sequence diagram shows how a single ndjson message travels from the external process to the rendered terminal frame.

```mermaid
sequenceDiagram
    participant Ext as External Process
    participant BG as Background Thread<br/>(src/external.rs)
    participant CH as mpsc Channel
    participant ML as Main Loop<br/>(src/main.rs)
    participant AN as Animation<br/>(set_params)
    participant CV as Canvas<br/>(apply_effects)
    participant TM as Terminal

    Ext->>BG: write ndjson line to stdin or file
    BG->>BG: parse JSON → ExternalParams
    BG->>CH: send(ExternalParams)
    Note over CH: Non-blocking — zero<br/>overhead when idle
    ML->>CH: try_recv() each frame
    CH-->>ML: Ok(ExternalParams)
    ML->>ML: ext_state.merge(params)
    ML->>ML: handle pending changes<br/>(animation, scale, render, color)
    ML->>ML: virtual_time += dt * speed
    ML->>AN: set_params(&ext_state.params)
    AN->>AN: apply semantic overrides
    ML->>CV: update(canvas, effective_dt, virtual_time)
    ML->>CV: apply_effects(intensity, hue_shift)
    CV->>TM: render() → write frame
```

### Frame Integration

Every frame, the main loop performs these steps in order:

1. **Drain the channel** — calls `try_recv()` in a loop until it returns `Empty`. All queued messages from the background thread are merged into `CurrentState`.
2. **Apply pending structural changes** — animation switch, scale rebuild, render mode, and color mode changes each trigger a canvas rebuild. A canvas rebuild skips the current frame and resumes on the next iteration.
3. **Compute virtual time**`virtual_time += dt * speed`. The speed value is clamped to `[0.1, 5.0]`.
4. **Call `set_params`** — passes the accumulated `ExternalParams` to the active animation. Most animations ignore this (default no-op).
5. **Call `update`** — the animation draws into the canvas using `effective_dt` and `virtual_time`.
6. **Call `apply_effects`** — post-processes the canvas with `intensity` and `hue_shift`.
7. **Render and write** — converts the canvas to a terminal escape sequence string and writes it to stdout.

### State Merging

`CurrentState` accumulates the most recent value of each field across all received messages. The merging strategy differs by field type:

```mermaid
graph LR
    subgraph "Persistent fields\n(accumulate across frames)"
        Speed["speed\nRetained until updated"]
        Intensity["intensity\nRetained until updated"]
        ColorShift["color_shift\nRetained until updated"]
    end

    subgraph "One-shot fields\n(consumed on first frame)"
        Anim["animation\nCleared after switch"]
        Scale["scale\nCleared after rebuild"]
        Render["render\nCleared after rebuild"]
        Color["color\nCleared after rebuild"]
    end

    style Speed fill:#1b5e20,stroke:#4caf50,stroke-width:2px,color:#ffffff
    style Intensity fill:#1b5e20,stroke:#4caf50,stroke-width:2px,color:#ffffff
    style ColorShift fill:#1b5e20,stroke:#4caf50,stroke-width:2px,color:#ffffff
    style Anim fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
    style Scale fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
    style Render fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
    style Color fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
```

**Persistent fields** (`speed`, `intensity`, `color_shift`) remain active until a new message explicitly changes them. Once you set `speed: 2.0`, every subsequent frame runs at double speed until you send a new speed value.

**One-shot fields** (`animation`, `scale`, `render`, `color`) are consumed the moment the main loop processes them. After the animation switch or canvas rebuild happens, the pending value is cleared.

---

## Animation Trait Extension

### Default Behavior

Every animation implements the `Animation` trait defined in `src/animations/mod.rs`. The trait includes a `set_params` method with a default no-op implementation:

```rust
/// Called once per frame before update(). Default is a no-op.
fn set_params(&mut self, _params: &crate::external::ExternalParams) {}
```

All 60 animations inherit this default. Most animations do not need to inspect external params because `speed`, `intensity`, and `color_shift` are handled globally by the main loop and canvas post-processing. Only animations that want to respond to a parameter **semantically** — wiring it to an internal simulation variable — need to override `set_params`.

### Semantic Overrides

Eight animations implement semantic overrides that give a field additional, animation-specific meaning beyond its global effect.

```mermaid
graph TD
    subgraph "Global effects (all animations)"
        GI["intensity → canvas brightness scale\napply_effects() post-processes every pixel"]
        GCS["color_shift → global hue rotation\napply_effects() rotates all colors"]
        GS["speed → virtual time multiplier\ndt * speed each frame"]
    end

    subgraph "fire (src/animations/fire.rs)"
        FI["intensity → heat_rate\nControls bottom-row burn temperature\nand vertical decay rate\n(higher = taller flames)"]
    end

    subgraph "plasma (src/animations/plasma.rs)"
        PCS["color_shift → hue_bias\nRotates plasma palette independently\nof the global hue shift"]
    end

    subgraph "boids (src/animations/boids.rs)"
        BI["intensity → cohes_factor\nFlock cohesion strength\n(0.001–0.05)"]
        BCS["color_shift → sep_factor\nSeparation force\n(0.5–5.0)"]
    end

    subgraph "particles (src/animations/particles.rs)"
        PI["intensity → gravity\nParticle gravity pull\n(0.0–40.0)"]
        PCS2["color_shift → drag\nAir resistance\n(0.9–1.0)"]
    end

    subgraph "wave (src/animations/wave.rs)"
        WI["intensity → amplitude\nWave height\n(0.1–1.0)"]
        WCS["color_shift → frequency\nWave frequency\n(0.05–0.8)"]
    end

    subgraph "sort, snake, pong (speed override)"
        SSI["speed → ops_per_frame / move_interval / speed_mult\nControls simulation pace"]
    end

    GI -.->|"also drives"| FI
    GI -.->|"also drives"| BI
    GI -.->|"also drives"| PI
    GI -.->|"also drives"| WI
    GCS -.->|"also drives"| PCS
    GCS -.->|"also drives"| BCS
    GCS -.->|"also drives"| PCS2
    GCS -.->|"also drives"| WCS
    GS -.->|"also drives"| SSI

    style GI fill:#37474f,stroke:#78909c,stroke-width:2px,color:#ffffff
    style GCS fill:#37474f,stroke:#78909c,stroke-width:2px,color:#ffffff
    style GS fill:#37474f,stroke:#78909c,stroke-width:2px,color:#ffffff
    style FI fill:#e65100,stroke:#ff9800,stroke-width:3px,color:#ffffff
    style PCS fill:#880e4f,stroke:#c2185b,stroke-width:2px,color:#ffffff
    style BI fill:#1b5e20,stroke:#4caf50,stroke-width:2px,color:#ffffff
    style BCS fill:#1b5e20,stroke:#4caf50,stroke-width:2px,color:#ffffff
    style PI fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
    style PCS2 fill:#0d47a1,stroke:#2196f3,stroke-width:2px,color:#ffffff
    style WI fill:#1a237e,stroke:#3f51b5,stroke-width:2px,color:#ffffff
    style WCS fill:#1a237e,stroke:#3f51b5,stroke-width:2px,color:#ffffff
    style SSI fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#ffffff
```

**fire — `intensity` maps to `heat_rate`**

When you send `intensity` while running the fire animation, the value is wired directly to the fire's internal `heat_rate`. A higher `heat_rate` makes the bottom row burn hotter and slows the vertical decay, producing taller flames. A lower value produces cooler, shorter flames. The global canvas brightness effect still applies on top of this.

```rust
fn set_params(&mut self, params: &crate::external::ExternalParams) {
    if let Some(intensity) = params.intensity {
        self.heat_rate = intensity.clamp(0.0, 2.0);
    }
}
```

**plasma — `color_shift` maps to `hue_bias`**

When you send `color_shift` while running the plasma animation, the value is wired to `hue_bias`, which rotates the plasma's internal color palette. This is independent of the global hue rotation applied by `apply_effects`. The result is that plasma responds to `color_shift` twice: once in its internal sine-wave color calculation (semantic) and once in the post-processing pass (global).

```rust
fn set_params(&mut self, params: &crate::external::ExternalParams) {
    if let Some(cs) = params.color_shift {
        self.hue_bias = cs.clamp(0.0, 1.0);
    }
}
```

**boids — `intensity` maps to `cohes_factor`, `color_shift` maps to `sep_factor`**

The boids flocking simulation wires `intensity` to the cohesion factor (how strongly boids pull toward the flock center, range 0.001–0.05) and `color_shift` to the separation factor (how strongly boids repel from nearby neighbors, range 0.5–5.0). Higher cohesion creates tighter flocks; higher separation creates more spaced-out formations.

**particles — `intensity` maps to `gravity`, `color_shift` maps to `drag`**

The fireworks particle system wires `intensity` to gravity (range 0.0–40.0, where 0 is weightless and 40 pulls particles down fast) and `color_shift` to the drag coefficient (range 0.9–1.0, where 1.0 is no drag and 0.9 applies strong air resistance).

**wave — `intensity` maps to `amplitude`, `color_shift` maps to `frequency`**

The sine wave interference pattern wires `intensity` to wave amplitude (range 0.1–1.0) and `color_shift` to wave frequency (range 0.05–0.8). This gives direct control over the wave shape in addition to the global brightness and hue effects.

**sort, snake, pong — `speed` maps to simulation pace**

Three game/simulation animations override `speed` with animation-specific meaning:
- **sort**: `speed` controls `ops_per_frame` (range 1–20), the number of sorting operations performed per frame.
- **snake**: `speed` controls `move_interval` (range 0.02–0.2 seconds), the delay between snake moves.
- **pong**: `speed` controls `speed_mult` (range 0.2–3.0), a multiplier on ball and paddle velocity.

> **✅ Tip:** To add semantic behavior to a new animation, override `set_params` in its `impl Animation` block. The method receives the full `ExternalParams` struct, so you can respond to any combination of fields.

---

## Configuration

The `data_file` path can be set in the config file so you do not need to pass `--data-file` on every launch.

Config file location: `~/.config/termflix/config.toml`

Generate the default config template with:

```bash
termflix --init-config
```

The relevant field in the config file:

```toml
# termflix configuration

# Watch a file for external control params (ndjson — one JSON object per line)
# data_file = "/tmp/termflix.json"
```

**Priority order for `data_file`:** CLI flag `--data-file` overrides the config file value.

> **📝 Note:** The config file accepts all standard options. Use `termflix --show-config` to display the active configuration and the path of the config file being used.

---

## Usage Examples

### Stdin Basics

**Switch to plasma at 3x speed:**

```bash
echo '{"animation":"plasma","speed":3.0}' | termflix
```

**Slow and dim fire:**

```bash
echo '{"animation":"fire","speed":0.3,"intensity":0.5}' | termflix
```

**Change render mode at launch:**

```bash
echo '{"render":"braille","speed":1.5}' | termflix fire
```

**Send multiple fields in one message:**

```bash
echo '{"animation":"plasma","speed":2.0,"intensity":1.2,"color_shift":0.1}' | termflix
```

### File-Based Control

Start termflix with a data file and update it from a separate shell:

```bash
# Step 1: Write an initial state
echo '{"animation":"fire","intensity":0.7}' > /tmp/tf.json

# Step 2: Launch termflix in the background
termflix --data-file /tmp/tf.json &

# Step 3: Update the animation after 2 seconds
sleep 2
echo '{"animation":"aurora","color_shift":0.3}' > /tmp/tf.json

# Step 4: Switch again
sleep 3
echo '{"animation":"plasma","speed":1.5,"intensity":1.1}' > /tmp/tf.json
```

> **✅ Tip:** Only the last non-empty line in the file is read on each file change event. You can append lines to build a history, or overwrite the file completely — either approach works.

### Continuous Control Loops

**Animate hue continuously using a shell loop:**

```bash
{
  while true; do
    val=$(python3 -c "import time, math; print(round(abs(math.sin(time.time())), 3))")
    echo "{\"color_shift\":${val}}"
    sleep 0.5
  done
} | termflix plasma
```

**Pulse intensity in sync with an external event:**

```bash
{
  for i in $(seq 1 10); do
    echo '{"intensity":2.0}'
    sleep 0.1
    echo '{"intensity":0.5}'
    sleep 0.4
  done
} | termflix fire
```

### Partial Updates

Only the fields you send are changed. All other state persists across messages:

```bash
# Speed up the current animation — animation name, intensity, etc. are unchanged
echo '{"speed":2.0}' | termflix

# Dim just the brightness — speed and animation are unchanged
echo '{"intensity":0.4}' | termflix aurora

# Combine a render mode switch with a speed change in one message
echo '{"render":"ascii","speed":0.8}' | termflix matrix
```

---

## Keyboard Shortcuts

Keyboard shortcuts remain fully active during external control. The keyboard and external control work on separate input paths and do not interfere.

| Key | Action |
|-----|--------|
| `q` or `Esc` | Quit |
| `` or `p` | Previous animation |
| `` or `n` | Next animation |
| `r` | Cycle render mode |
| `c` | Cycle color mode |
| `b` | Toggle bloom post-processing |
| `h` | Toggle status bar |

> **📝 Note:** When using stdin mode, the keyboard still works because keyboard events are read by crossterm through the terminal device (`/dev/tty`), not through stdin. Stdin is consumed separately by the background reader thread.

---

## Error Handling

termflix uses a silent-discard strategy for all external control errors. The player never crashes or logs error messages to the screen due to malformed input.

```mermaid
stateDiagram-v2
    [*] --> ReadLine: background thread reads line
    ReadLine --> ParseJSON: line received
    ParseJSON --> ValidFields: JSON is valid
    ParseJSON --> Discard: JSON is invalid → silently skipped
    ValidFields --> CheckAnimation: animation field present
    CheckAnimation --> SwitchAnim: name is known
    CheckAnimation --> KeepCurrent: name is unknown → silently ignored
    ValidFields --> ApplyFields: speed / intensity / color_shift
    ValidFields --> CheckRender: render field present
    CheckRender --> SetRender: value is recognized
    CheckRender --> IgnoreRender: value is unrecognized → silently ignored
    ValidFields --> CheckColor: color field present
    CheckColor --> SetColor: value is recognized
    CheckColor --> IgnoreColor: value is unrecognized → silently ignored
    SwitchAnim --> [*]
    KeepCurrent --> [*]
    ApplyFields --> [*]
    SetRender --> [*]
    IgnoreRender --> [*]
    SetColor --> [*]
    IgnoreColor --> [*]
    Discard --> [*]
```

**Summary of silent-discard cases:**

| Input | Behavior |
|-------|----------|
| Invalid JSON (syntax error) | Line is skipped, player continues |
| Unknown animation name | Animation unchanged, no error |
| Unknown `render` value | Render mode unchanged, no error |
| Unknown `color` value | Color mode unchanged, no error |
| Field value out of range | Clamped to the nearest valid bound |
| EOF on stdin | Background thread exits cleanly |
| File deleted while watching | Background thread exits, player continues |

> **⚠️ Warning:** Because all errors are silent, validate your ndjson messages with a tool like `jq` before piping to termflix if you are not seeing the expected behavior. A quick check: `echo '{"animation":"fire"}' | jq .` should return the parsed object without error.

---

## Source Files

| File | Role |
|------|------|
| `src/external.rs` | `ExternalParams`, `CurrentState`, `spawn_reader`, `ParamsSource` |
| `src/animations/mod.rs` | `Animation` trait with default no-op `set_params` |
| `src/animations/fire.rs` | Semantic `set_params` override — `intensity``heat_rate` |
| `src/animations/plasma.rs` | Semantic `set_params` override — `color_shift``hue_bias` |
| `src/animations/boids.rs` | Semantic `set_params` override — `intensity``cohes_factor`, `color_shift``sep_factor` |
| `src/animations/particles.rs` | Semantic `set_params` override — `intensity``gravity`, `color_shift``drag` |
| `src/animations/wave.rs` | Semantic `set_params` override — `intensity``amplitude`, `color_shift``frequency` |
| `src/animations/sort.rs` | Semantic `set_params` override — `speed``ops_per_frame` |
| `src/animations/snake.rs` | Semantic `set_params` override — `speed``move_interval` |
| `src/animations/pong.rs` | Semantic `set_params` override — `speed``speed_mult` |
| `src/render/canvas.rs` | `apply_effects` — intensity scaling and RGB→HSV hue rotation |
| `src/config.rs` | `data_file` config field and `Config` struct |
| `src/main.rs` | `--data-file` CLI flag, channel setup, per-frame integration |

---

## Related Documentation

- [README.md]../README.md — Project overview, installation, and quick start
- [DOCUMENTATION_STYLE_GUIDE.md]DOCUMENTATION_STYLE_GUIDE.md — Documentation standards for this project