Plugin protocols
Tinct supports two plugin communication protocols with automatic detection.
Protocol comparison
| Feature | JSON-stdio | Go-Plugin (RPC) |
|---|---|---|
| Languages | Any (shell, Python, Ruby, etc.) | Go |
| Communication | JSON over stdin/stdout | RPC over stdio |
| Process model | New process per invocation | Persistent process (reused) |
| Startup cost | High (fork+exec each time) | Low (RPC to running process) |
| Isolation | Basic process isolation | Enhanced with crash recovery |
| Health checks | None | Automatic monitoring |
| Error handling | stderr text | Structured RPC errors |
| Bidirectional | No | Yes (plugin can call back) |
| State | Stateless | Can maintain state |
| Dependencies | None | hashicorp/go-plugin |
| Complexity | Minimal | Moderate |
Automatic detection
Tinct automatically detects which protocol to use:
- Runs
plugin --plugin-info - Parses the
plugin_protocolfield - Uses the appropriate executor
No configuration required.
JSON-stdio protocol
Best for simple plugins written in any language.
When to use
- Writing plugins in shell scripts, Python, Ruby, or any language
- Plugin is simple and short-lived
- Maximum portability
- Minimal dependencies
How it works
1. Plugin info query
$ my-plugin --plugin-info
Returns:
{
"name": "my-plugin",
"type": "output",
"version": "1.0.0",
"protocol_version": "0.0.1",
"description": "My plugin",
"plugin_protocol": "json-stdio"
}
2. Execution flow
tinct plugin
│ │
│──── spawn process ──────────▶│
│ │
│──── JSON via stdin ─────────▶│
│ │
│◀─── JSON via stdout ─────────│
│ │
│◀─── process exits ───────────│
│ │
Input plugin data format
Input plugins receive configuration and must return palette data:
Stdin (configuration):
{
"source": "/path/to/image.jpg",
"options": {
"colours": 16,
"algorithm": "kmeans"
}
}
Stdout (palette):
{
"colours": {
"background": {"hex": "#1e1e2e", "rgb": {"r": 30, "g": 30, "b": 46}},
"foreground": {"hex": "#cdd6f4", "rgb": {"r": 205, "g": 214, "b": 244}},
"accent1": {"hex": "#89b4fa", "rgb": {"r": 137, "g": 180, "b": 250}}
},
"theme_type": "dark",
"wallpaper_path": "/path/to/image.jpg"
}
Output plugin data format
Output plugins receive the full palette:
Stdin (palette):
{
"colours": {
"background": {
"role": "background",
"hex": "#1e1e2e",
"rgb": {"r": 30, "g": 30, "b": 46},
"rgba": {"r": 30, "g": 30, "b": 46, "a": 255},
"luminance": 0.027,
"is_light": false,
"hue": 240,
"saturation": 0.21
},
"foreground": {
"role": "foreground",
"hex": "#cdd6f4",
"rgb": {"r": 205, "g": 214, "b": 244},
"rgba": {"r": 205, "g": 214, "b": 244, "a": 255}
}
},
"theme_type": "dark",
"all_colours": [...],
"wallpaper_path": "/path/to/wallpaper.jpg"
}
Stdout (status):
{
"success": true,
"files_written": [
"/home/user/.config/myapp/colours.conf"
],
"message": "Generated configuration"
}
Error handling
Write errors to stderr:
echo "Error: config directory not found" >&2
exit 1
Go-plugin protocol
Best for performance-critical Go plugins.
When to use
- Writing plugins in Go
- Plugin does heavy computation
- Need better error handling and crash recovery
- Plugin maintains state between invocations
- Want health monitoring
How it works
1. Plugin info query (same as JSON-stdio)
$ my-plugin --plugin-info
Returns:
{
"name": "my-plugin",
"type": "output",
"version": "1.0.0",
"protocol_version": "0.0.1",
"description": "My Go plugin",
"plugin_protocol": "go-plugin"
}
2. Execution flow
tinct plugin (persistent)
│ │
│──── spawn process ──────────▶│
│ │
│──── RPC: Generate() ────────▶│
│◀─── RPC response ────────────│
│ │
│──── RPC: Generate() ────────▶│ (reused)
│◀─── RPC response ────────────│
│ │
│──── RPC: Kill() ────────────▶│
│◀─── process exits ───────────│
Performance comparison
| Protocol | 100 Invocations | Avg per Call |
|---|---|---|
| JSON-stdio | ~5.2s | ~52ms |
| Go-Plugin | ~0.8s | ~8ms |
Go-Plugin is approximately 6x faster for repeated invocations due to process reuse.
RPC interface
Output plugins implement:
type OutputPlugin interface {
// Generate creates configuration files from palette data
Generate(ctx context.Context, palette PaletteData) (map[string][]byte, error)
// PreExecute validates environment (optional)
PreExecute(ctx context.Context) (skip bool, reason string, err error)
// PostExecute runs after file generation (optional)
PostExecute(ctx context.Context, files []string) error
// GetMetadata returns plugin information
GetMetadata() PluginInfo
}
Input plugins implement:
type InputPlugin interface {
// Extract generates a palette from the source
Extract(ctx context.Context, config InputConfig) (*PaletteData, error)
// GetMetadata returns plugin information
GetMetadata() PluginInfo
// WallpaperPath returns path to wallpaper (optional)
WallpaperPath() string
}
Handshake configuration
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "TINCT_PLUGIN",
MagicCookieValue: "tinct",
}
Wallpaper support
Input plugins can provide wallpaper images to output plugins.
JSON-stdio format
Return wallpaper_path in the response:
{
"colours": [...],
"wallpaper_path": "/path/to/wallpaper.png"
}
Go-plugin format
Implement the WallpaperPath() method:
func (p *MyInputPlugin) WallpaperPath() string {
return p.imagePath
}
Output plugins receive the wallpaper path in the palette data and can use it for applications like hyprpaper.
Protocol version
The current protocol version is 0.0.1. Plugins should declare this in their metadata:
{
"protocol_version": "0.0.1"
}
Tinct checks version compatibility and warns if a plugin uses an incompatible version.
See also
- Creating plugins - Implementation examples
- Lifecycle hooks - Pre/post execution
- HashiCorp go-plugin - RPC framework