Skip to main content
Version: 0.2.2

Plugin protocols

Tinct supports two plugin communication protocols with automatic detection.

Protocol comparison

FeatureJSON-stdioGo-Plugin (RPC)
LanguagesAny (shell, Python, Ruby, etc.)Go
CommunicationJSON over stdin/stdoutRPC over stdio
Process modelNew process per invocationPersistent process (reused)
Startup costHigh (fork+exec each time)Low (RPC to running process)
IsolationBasic process isolationEnhanced with crash recovery
Health checksNoneAutomatic monitoring
Error handlingstderr textStructured RPC errors
BidirectionalNoYes (plugin can call back)
StateStatelessCan maintain state
DependenciesNonehashicorp/go-plugin
ComplexityMinimalModerate

Automatic detection

Tinct automatically detects which protocol to use:

  1. Runs plugin --plugin-info
  2. Parses the plugin_protocol field
  3. 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.2.0",
"description": "My plugin",
"plugin_protocol": "json-stdio"
}

2. Execution flow

tinct                          plugin
│ │
│──── spawn process ──────────▶│
│ │
│──── JSON via stdin ─────────▶│
│ │
│◀─── JSON via stdout ─────────│
│ (status to stderr) ──────│
│ │
│◀─── 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 via stdin and must write a structured JSON response to stdout.

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 (structured response — protocol 0.2.0):

{
"success": true,
"files_written": [
"/home/user/.config/myapp/colours.conf"
],
"message": "Generated configuration"
}
FieldTypeRequiredDescription
successbooleanYesWhether the plugin completed successfully
files_writtenstring[]YesAbsolute paths of files written by the plugin (empty array if none)
messagestringNoHuman-readable status message

Important: Plugins must write all informational/status output to stderr. Only the JSON response goes to stdout. Tinct displays stderr output to the user.

For plugins that report failure, set success to false with an explanatory message. The plugin should still exit with code 0 — Tinct treats non-zero exit codes as a process-level crash, distinct from a plugin-reported failure.

Error handling

Write errors and status messages to stderr:

echo "Generating theme configuration..." >&2
echo "Error: config directory not found" >&2

For fatal errors, either write a failure response to stdout or exit non-zero:

# Option 1: Structured failure (preferred)
echo '{"success":false,"files_written":[],"message":"Config directory not found"}'
exit 0

# Option 2: Process-level error (last resort)
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.2.0",
"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

Protocol100 InvocationsAvg 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.2.0. Plugins should declare this in their metadata:

{
"protocol_version": "0.2.0"
}

Version history

VersionChanges
0.2.0Structured JSON response for output plugins (success, files_written, message). Plugins write files themselves and report paths. Informational output goes to stderr only.
0.0.1Initial version. Output plugin stdout treated as freeform text, displayed to user. No file tracking for json-stdio plugins.

Tinct checks version compatibility and warns if a plugin uses an incompatible version. Plugins using protocol versions older than 0.2.0 continue to work with legacy behavior (freeform stdout displayed to user, no file tracking).

See also