Skip to main content
Version: 0.1.22

Creating plugins

Write external plugins in any language.

Shell script plugin

The simplest way to create a plugin:

#!/bin/bash
# tinct-output-my-plugin

set -e

if [[ "$1" == "--plugin-info" ]]; then
cat <<EOF
{
"name": "my-plugin",
"type": "output",
"version": "1.0.0",
"protocol_version": "0.0.1",
"description": "Example shell plugin",
"plugin_protocol": "json-stdio"
}
EOF
exit 0
fi

# Read palette from stdin
palette=$(cat)

# Extract colours using jq
bg=$(echo "$palette" | jq -r '.colours.background.hex')
fg=$(echo "$palette" | jq -r '.colours.foreground.hex')
accent=$(echo "$palette" | jq -r '.colours.accent1.hex')

# Generate output
mkdir -p ~/.config/myapp
cat > ~/.config/myapp/colours.conf <<EOF
background=$bg
foreground=$fg
accent=$accent
EOF

echo "Generated ~/.config/myapp/colours.conf"

Make it executable and install:

chmod +x tinct-output-my-plugin
cp tinct-output-my-plugin ~/.config/tinct/plugins/

Python plugin

#!/usr/bin/env python3
"""Tinct output plugin for my application."""

import json
import sys
import os

def get_plugin_info():
return {
"name": "my-plugin",
"type": "output",
"version": "1.0.0",
"protocol_version": "0.0.1",
"description": "Example Python plugin",
"plugin_protocol": "json-stdio"
}

def generate(palette):
"""Generate configuration from palette."""
colours = palette['colours']

config = f"""# Generated by tinct
background = {colours['background']['hex']}
foreground = {colours['foreground']['hex']}
accent = {colours['accent1']['hex']}
danger = {colours['danger']['hex']}
success = {colours['success']['hex']}
"""

config_path = os.path.expanduser("~/.config/myapp/colours.conf")
os.makedirs(os.path.dirname(config_path), exist_ok=True)

with open(config_path, 'w') as f:
f.write(config)

# Return success response
response = {
"success": True,
"files_written": [config_path],
"message": f"Generated {config_path}"
}
print(json.dumps(response))

def main():
if len(sys.argv) > 1 and sys.argv[1] == "--plugin-info":
print(json.dumps(get_plugin_info(), indent=2))
return

# Read palette from stdin
palette = json.load(sys.stdin)
generate(palette)

if __name__ == "__main__":
main()

Go plugin (JSON-stdio)

For simple Go plugins using the JSON-stdio protocol:

package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)

type PluginInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Version string `json:"version"`
ProtocolVersion string `json:"protocol_version"`
Description string `json:"description"`
PluginProtocol string `json:"plugin_protocol"`
}

type Colour struct {
Hex string `json:"hex"`
RGB struct {
R, G, B int `json:"r,g,b"`
} `json:"rgb"`
}

type Palette struct {
Colours map[string]Colour `json:"colours"`
}

func main() {
if len(os.Args) > 1 && os.Args[1] == "--plugin-info" {
info := PluginInfo{
Name: "my-plugin",
Type: "output",
Version: "1.0.0",
ProtocolVersion: "0.0.1",
Description: "Example Go plugin",
PluginProtocol: "json-stdio",
}
json.NewEncoder(os.Stdout).Encode(info)
return
}

var palette Palette
if err := json.NewDecoder(os.Stdin).Decode(&palette); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

bg := palette.Colours["background"].Hex
fg := palette.Colours["foreground"].Hex

configDir := filepath.Join(os.Getenv("HOME"), ".config", "myapp")
os.MkdirAll(configDir, 0755)

configPath := filepath.Join(configDir, "colours.conf")
content := fmt.Sprintf("background=%s\nforeground=%s\n", bg, fg)

os.WriteFile(configPath, []byte(content), 0644)
fmt.Printf("Generated %s\n", configPath)
}

Go plugin (go-plugin RPC)

For high-performance Go plugins using HashiCorp's go-plugin:

package main

import (
"context"
"fmt"
"os"

"github.com/hashicorp/go-plugin"
"github.com/jmylchreest/tinct/pkg/plugin/protocol"
)

type MyPlugin struct{}

func (p *MyPlugin) Generate(ctx context.Context, palette protocol.PaletteData) (map[string][]byte, error) {
bg := palette.Colours["background"].Hex
fg := palette.Colours["foreground"].Hex
accent := palette.Colours["accent1"].Hex

content := fmt.Sprintf(`# Generated by tinct
background = %s
foreground = %s
accent = %s
`, bg, fg, accent)

return map[string][]byte{
"colours.conf": []byte(content),
}, nil
}

func (p *MyPlugin) PreExecute(ctx context.Context) (bool, string, error) {
// Optional: Check if app is installed
return false, "", nil
}

func (p *MyPlugin) PostExecute(ctx context.Context, execCtx protocol.ExecutionContext, files []string) error {
// Optional: Reload app configuration
return nil
}

func (p *MyPlugin) GetMetadata() protocol.PluginInfo {
return protocol.PluginInfo{
Name: "my-plugin",
Type: "output",
Version: "1.0.0",
ProtocolVersion: protocol.ProtocolVersion,
Description: "Example Go plugin",
PluginProtocol: "go-plugin",
}
}

func main() {
// Handle --plugin-info for discovery
if len(os.Args) > 1 && os.Args[1] == "--plugin-info" {
p := &MyPlugin{}
info := p.GetMetadata()
fmt.Printf(`{
"name": "%s",
"type": "%s",
"version": "%s",
"protocol_version": "%s",
"description": "%s",
"plugin_protocol": "%s"
}
`, info.Name, info.Type, info.Version, info.ProtocolVersion,
info.Description, info.PluginProtocol)
os.Exit(0)
}

// Serve via go-plugin
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: protocol.Handshake,
Plugins: map[string]plugin.Plugin{
"output": &protocol.OutputPluginRPC{
Impl: &MyPlugin{},
},
},
})
}

Build and install:

go build -o tinct-output-my-plugin
cp tinct-output-my-plugin ~/.config/tinct/plugins/

Accessing colour data

Available colour fields

Each colour in the palette includes:

{
"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,
"index": 0,
"is_generated": false,
"weight": 0.42
}

Common colour roles

# Core colours
colours['background']['hex'] # Base background
colours['foreground']['hex'] # Primary text
colours['backgroundMuted']['hex'] # Inactive background
colours['foregroundMuted']['hex'] # Secondary text

# Accent colours
colours['accent1']['hex'] # Primary accent
colours['accent2']['hex'] # Secondary accent
colours['accent3']['hex'] # Tertiary accent
colours['accent4']['hex'] # Quaternary accent

# Semantic colours
colours['danger']['hex'] # Errors, destructive
colours['warning']['hex'] # Warnings, caution
colours['success']['hex'] # Success, confirmation
colours['info']['hex'] # Information

# Surface colours
colours['surface']['hex'] # Card backgrounds
colours['onSurface']['hex'] # Text on surface
colours['outline']['hex'] # Borders, dividers

Wallpaper path

If the input plugin provides a wallpaper, it's available at:

palette['wallpaper_path']  # e.g., "/home/user/wallpaper.jpg"

Useful for plugins like hyprpaper that set wallpapers.

Testing plugins

Manual testing

# Test plugin info
./tinct-output-my-plugin --plugin-info

# Test with sample palette
cat sample-palette.json | ./tinct-output-my-plugin

# Create a sample palette
cat > sample-palette.json <<EOF
{
"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"
}
EOF

Testing with tinct

# Enable verbose mode to see plugin output
tinct generate -i image -p wallpaper.jpg -o my-plugin --verbose

# Dry run (don't write files)
tinct generate -i image -p wallpaper.jpg -o my-plugin --dry-run

Best practices

  1. Always handle --plugin-info - Required for plugin discovery
  2. Read from stdin - Palette data comes via stdin as JSON
  3. Write status to stdout - Success messages and responses
  4. Write errors to stderr - Error messages go to stderr
  5. Use proper exit codes - Return non-zero on failure
  6. Validate input - Check palette data before using
  7. Create directories - Use mkdir -p or equivalent
  8. Handle missing colours - Not all roles may be present

See also