Building Hamr Plugins¶
This guide will help you build your first Hamr plugin. Start here, then explore the detailed documentation as needed.
What You'll Build¶
Hamr plugins extend the launcher with custom functionality. When you're done with this guide, you'll have created a working plugin that:
- Shows a list of items when opened
- Filters items as you type
- Executes an action when you select an item

Quick Start: Your First Plugin¶
Let's build a "Hello World" plugin in under 5 minutes.
Step 1: Create the Plugin Directory¶
Hamr loads plugins from two locations:
- Built-in plugins:
~/.local/share/hamr/plugins/(installed with Hamr) - User plugins:
~/.config/hamr/plugins/(your custom plugins)
Create your plugin in the user directory:
Step 2: Create the Manifest¶
The manifest tells Hamr about your plugin. Important: You must specify supportedPlatforms or your plugin won't appear.
cat > ~/.config/hamr/plugins/hello/manifest.json << 'EOF'
{
"name": "Hello",
"description": "My first Hamr plugin",
"icon": "waving_hand",
"supportedPlatforms": ["niri", "hyprland"]
}
EOF
Note:
supportedPlatformsdefines which platforms/compositors your plugin works with:
["niri", "hyprland"]- Linux Wayland compositors["hyprland"]- Hyprland only (if you usehyprctl)["niri"]- Niri only (if you useniri msg)["macos"]- macOS["windows"]- WindowsList all platforms your plugin supports explicitly. There is no wildcard.
Note: For stdio plugins, Hamr always runs
handler.pyin the plugin directory, so thehandlerfield is optional. Usehandler.commandonly for socket/daemon plugins.
Step 3: Create the Handler¶
The handler is your plugin's logic. Create ~/.config/hamr/plugins/hello/handler.py:
#!/usr/bin/env python3
"""
Hamr Plugin Handler Template
Hamr communicates with plugins via JSON over stdin/stdout:
1. Hamr spawns your handler as a subprocess
2. Hamr writes a JSON request to your handler's stdin
3. Your handler reads the request, processes it, and prints a JSON response to stdout
4. Hamr reads the response and updates the UI
This stateless request-response model means each invocation is independent.
Use the `context` field to persist state across calls if needed.
"""
import json
import sys
def main():
# Read the JSON request from stdin
# Hamr sends the entire request as a single JSON object
input_data = json.load(sys.stdin)
# Extract common fields from the request
step = input_data.get("step", "initial") # What triggered this call
query = input_data.get("query", "").strip() # Search bar text (for search step)
selected = input_data.get("selected", {}) # Selected item info (for action step)
# Define our items (in a real plugin, this might come from a file or API)
items = [
{"id": "hello", "name": "Say Hello", "icon": "waving_hand", "description": "Show a greeting"},
{"id": "goodbye", "name": "Say Goodbye", "icon": "door_open", "description": "Show a farewell"},
]
# STEP: initial - Plugin just opened, show the initial view
if step == "initial":
print(json.dumps({
"type": "results",
"results": items,
"placeholder": "Search greetings..."
}))
return
# STEP: search - User is typing, filter results
if step == "search":
query_lower = query.lower()
filtered = [i for i in items if query_lower in i["name"].lower()]
print(json.dumps({
"type": "results",
"results": filtered
}))
return
# STEP: action - User selected an item or clicked an action button
if step == "action":
item_id = selected.get("id", "")
message = "Hello, World!" if item_id == "hello" else "Goodbye!"
# Return an execute response to perform an action
print(json.dumps({
"type": "execute",
"notify": message, # Show a notification
"close": True # Close the launcher
}))
if __name__ == "__main__":
main()
Step 4: Make it Executable¶
Step 5: Test Your Plugin¶
Open Hamr and search for "hello" to see your plugin in action.
Tip: If your plugin doesn't appear, check:
supportedPlatformsis set in manifest.json- The handler is executable (
chmod +x) - Check logs:
journalctl --user -u hamr -f
How Plugins Work¶
Communication Model¶
Plugins communicate with Hamr via JSON over stdin/stdout:
sequenceDiagram
participant Hamr
participant Handler as Your Handler
Hamr->>Handler: Spawn process
Hamr->>Handler: Write JSON request to stdin
Handler->>Handler: Process request
Handler->>Hamr: Print JSON response to stdout
Hamr->>Hamr: Update UI
Handler->>Handler: Exit
- Hamr spawns your handler as a subprocess
- Hamr writes a JSON request to your handler's stdin
- Your handler processes the request
- Your handler prints a JSON response to stdout
- Hamr reads the response and updates the UI
- The handler process exits
The Request-Response Cycle¶
Every request includes a step field telling you what happened:
| Step | When | User Action |
|---|---|---|
initial |
Plugin opens | User selected the plugin |
search |
User types | Each keystroke (realtime) or Enter (submit) |
action |
User selects | Clicked item or pressed Enter |
Plugin Discovery¶
Hamr scans these directories for plugins on startup:
flowchart LR
A[Hamr Starts] --> B{Scan Directories}
B --> C[~/.local/share/hamr/plugins/]
B --> D[~/.config/hamr/plugins/]
C --> E{Valid manifest.json?}
D --> E
E -->|Yes| F{supportedPlatforms<br/>matches current?}
E -->|No| G[Skip]
F -->|Yes| H[Load Plugin]
F -->|No| G
Key points:
- Each plugin must have a
manifest.json supportedPlatformsmust include the current platform- Handler must be executable with a valid shebang
Directory Structure¶
~/.config/hamr/plugins/
├── my-plugin/
│ ├── manifest.json # Plugin metadata (required)
│ └── handler.py # Plugin logic (required)
Core Concepts¶
The Manifest File¶
Every plugin needs a manifest.json:
{
"name": "My Plugin",
"description": "What it does",
"icon": "star",
"supportedPlatforms": ["niri", "hyprland"]
}
| Field | Required | Description |
|---|---|---|
name |
Yes | Display name |
description |
Yes | Short description |
icon |
Yes | Material icon name |
supportedPlatforms |
Yes | ["niri", "hyprland"], ["macos"], etc. (list all explicitly) |
handler |
No | Handler config. Stdio plugins run handler.py by default; socket plugins use handler.command. |
frecency |
No | "item", "plugin", or "none" (default: "item") |
Input (What You Receive)¶
{
"step": "initial|search|action", # What happened
"query": "user input", # Search bar text (search step)
"selected": {"id": "item-id"}, # Selected item (action step)
"action": "button-id", # Action button ID (action step, optional)
"context": "your-state", # Your custom state (persisted)
"session": "session-id" # Unique session identifier
}
The action field tells you which action button was clicked:
- Not set: User clicked the item itself (default action)
- Set: User clicked a specific action button (e.g.,
"copy","delete")
Output (What You Return)¶
Return one JSON object. The type field determines what Hamr does:
| Type | Purpose | Example |
|---|---|---|
results |
Show a list | Search results, menu items |
execute |
Run an action | Open file, copy text, close launcher |
card |
Show rich content | Markdown text, definitions |
error |
Show error | Something went wrong |
See Response Types for complete documentation.
Multi-step Flows (Optional)¶
For drill-down workflows (edit screens, pickers), store state in context and control the stack with navigateForward and navigateBack. Handle selected.id == "__back__" to return to the previous view. See Response Types for examples.
Language Support¶
Plugins can be written in any language. The handler just needs to:
-
Be executable (
chmod +x) -
Have a shebang (
#!/usr/bin/env python3) -
Read JSON from stdin
-
Write JSON to stdout
| Language | Use Case |
|---|---|
| Python | Recommended for most plugins |
| Bash | Simple scripts, system commands |
| Go/Rust | Performance-critical plugins |
| Node.js | Web API integrations |
Python (Recommended)¶
#!/usr/bin/env python3
import json
import sys
def main():
# Read JSON request from stdin (Hamr sends it all at once)
input_data = json.load(sys.stdin)
# Process and respond
print(json.dumps({"type": "results", "results": [...]}))
if __name__ == "__main__":
main()
Bash¶
#!/bin/bash
INPUT=$(cat)
STEP=$(echo "$INPUT" | jq -r '.step // "initial"')
case "$STEP" in
initial)
echo '{"type": "results", "results": [{"id": "1", "name": "Item", "icon": "star"}]}'
;;
esac
Node.js¶
#!/usr/bin/env node
const fs = require("fs");
const input = JSON.parse(fs.readFileSync(0, "utf-8"));
if (input.step === "initial") {
console.log(
JSON.stringify({
type: "results",
results: [{ id: "1", name: "Item", icon: "star" }],
}),
);
}
Next Steps¶
Now that you understand the basics:
-
Response Types - Learn about all response types (
results,execute,card,form, etc.) -
Visual Elements - Add sliders, switches, badges, gauges, and progress bars
-
Advanced Features - Pattern matching, daemon mode, indexing, search ranking
-
API Reference - Complete schema reference for all fields and types
-
Cheat Sheet - Quick reference for common patterns
Built-in Plugin Examples¶
Study these plugins to learn common patterns:
| Plugin | Features | Good For Learning |
|---|---|---|
calculate/ |
Pattern matching, instant results | Main search integration |
url/ |
Pattern matching, actions | Simple match handler |
timer/ |
Daemon, FAB override, ambient items, status | Advanced daemon patterns |
quicklinks/ |
CRUD, context, input modes | State management |
todo/ |
Daemon, file watching, status | Real-time updates |
clipboard/ |
Thumbnails, filters, actions | Rich UI |
bitwarden/ |
Forms, caching, entryPoint | Complex workflows |
sound/ |
Sliders, switches, updates | Interactive controls |
emoji/ |
Grid browser | Large item sets |
Development Mode¶
For plugin development, run Hamr in dev mode:
Dev mode:
- Auto-reloads when plugin files change
- Shows logs directly in the terminal
- Displays errors in the UI
See Testing Plugins for more details.
Troubleshooting¶
Plugin doesn't appear in Hamr¶
- Check
supportedPlatforms- Must be set in manifest.json
- Check file permissions - Handler must be executable
- Check for JSON errors - Validate your manifest
- Check logs - See what Hamr reports
Plugin shows error¶
- Test manually - Run your handler directly
- Check JSON output - Must be valid JSON
- Check terminal/logs - Errors appear in dev terminal or journalctl
Tips for Success¶
- Use dev mode - Run
./devfor auto-reload and live logs - Start simple - Get basic results working before adding features
- Test visually - Interact with your plugin; errors are shown in the UI
- Keep results under 50 - More items slow down the UI
- Use placeholder text - Helps users know what to type
- Handle empty states - Show helpful messages when no results match
- Desktop files - If you read
.desktopfiles, note that XDG defaults can point toNoDisplay=trueentries