Python SDK Reference¶
The Python SDK (hamr_sdk.py) provides helpers for building socket-based daemon plugins that communicate with the Hamr daemon via JSON-RPC 2.0.
Installation¶
The SDK is included in the plugins/sdk/ directory. Import it in your plugin:
import sys
from pathlib import Path
# Add parent directory to path to import SDK
sys.path.insert(0, str(Path(__file__).parent.parent))
from sdk.hamr_sdk import HamrPlugin
Quick Start¶
from sdk.hamr_sdk import HamrPlugin
plugin = HamrPlugin(
id="my-plugin",
name="My Plugin",
description="A socket-based plugin",
icon="extension"
)
@plugin.on_search
def handle_search(query: str, context: str | None) -> dict:
return HamrPlugin.results([
{"id": "1", "name": "Result", "icon": "star"}
])
@plugin.on_action
def handle_action(item_id: str, action: str | None, context: str | None, source: str | None) -> dict:
return HamrPlugin.copy_and_close("Hello")
plugin.run()
HamrPlugin Class¶
Constructor¶
HamrPlugin(
id: str, # Plugin identifier (matches manifest)
name: str, # Display name
description: str | None = None, # Plugin description
icon: str | None = None, # Material icon name
prefix: str | None = None, # Search prefix (e.g., "!")
priority: int = 0, # Search result priority
socket_path: str | None = None, # Custom socket path (defaults to $XDG_RUNTIME_DIR/hamr.sock)
debug: bool | None = None, # Enable debug logging (defaults to HAMR_PLUGIN_DEBUG env var)
)
Handler Decorators¶
Register handlers using decorators:
| Decorator | Handler Signature | Description |
|---|---|---|
@plugin.on_initial |
(params: dict) -> dict |
Called when plugin is opened |
@plugin.on_search |
(query: str, context: str \| None) -> dict |
Called on search input |
@plugin.on_action |
(item_id: str, action: str \| None, context: str \| None, source: str \| None) -> dict |
Called on item selection or action |
@plugin.on_form_submitted |
(form_data: dict, context: str \| None) -> dict |
Called on form submission |
@plugin.on_slider_changed |
(slider_id: str, value: float) -> dict |
Called on slider value change |
@plugin.on_switch_toggled |
(switch_id: str, value: float) -> dict |
Called on switch toggle (value is 1.0 or 0.0) |
Handlers can be sync or async:
@plugin.on_search
def sync_handler(query: str, context: str | None) -> dict:
return HamrPlugin.results([...])
@plugin.on_search
async def async_handler(query: str, context: str | None) -> dict:
result = await fetch_data(query)
return HamrPlugin.results([...])
Background Tasks¶
Add background coroutines that run alongside message handling:
@plugin.add_background_task
async def monitor_changes(p: HamrPlugin):
"""Background task receives plugin instance for sending updates."""
while True:
await asyncio.sleep(1)
# Send status update
await p.send_status({"chips": [{"text": "Updated"}]})
Background tasks are started after registration and run until the plugin exits.
Notification Methods¶
Send notifications to the daemon (no response expected):
# Send search results
await plugin.send_results(results=[...], **kwargs)
# Send status update (badges, chips, ambient items)
await plugin.send_status({"chips": [...], "badges": [...], "ambient": [...]})
# Send index items for search indexing
await plugin.send_index(items=[...])
# Request action execution
await plugin.send_execute({"type": "copy", "text": "Hello"})
# Send partial result updates (patches)
await plugin.send_update(patches=[{"id": "item1", "description": "Updated"}])
Response Builders¶
Static methods for building properly-typed response dictionaries:
HamrPlugin.results()¶
Build a results response:
HamrPlugin.results(
items: list[dict], # Required: result items
input_mode: str | None = None, # "realtime" for keystroke updates
status: dict | None = None, # Status with chips/badges/ambient
context: str | None = None, # Context passed to subsequent handlers
placeholder: str | None = None, # Search input placeholder
clear_input: bool = False, # Clear search input
navigate_forward: bool | None = None, # Push navigation state
plugin_actions: list[dict] | None = None, # Action bar actions
navigation_depth: int | None = None, # Navigation depth hint
display_hint: str | None = None, # "auto", "list", "grid", "large_grid"
)
HamrPlugin.form()¶
Build a form response:
HamrPlugin.form(
form: dict, # Form definition with title, fields
context: str | None = None, # Context for form submission
)
Example form:
HamrPlugin.form({
"title": "Settings",
"fields": [
{"type": "text", "id": "name", "label": "Name", "value": "Default"},
{"type": "switch", "id": "enabled", "label": "Enable feature", "value": True},
{"type": "slider", "id": "volume", "label": "Volume", "value": 50, "min": 0, "max": 100},
],
"submitLabel": "Save"
})
HamrPlugin.card()¶
Build a card response (detail view):
HamrPlugin.card(
title: str, # Required: card title
content: str | None = None, # Plain text content
markdown: str | None = None, # Markdown content
actions: list[dict] | None = None, # Action buttons
status: dict | None = None, # Status indicators
kind: str | None = None, # Card kind
blocks: list[dict] | None = None, # Card blocks (pill, separator, etc.)
max_height: int | None = None, # Max height in pixels
show_details: bool | None = None, # Show details section
allow_toggle_details: bool | None = None, # Allow toggling details
)
HamrPlugin.execute()¶
Build an execute response:
HamrPlugin.execute(
launch: str | None = None, # Desktop file to launch
copy: str | None = None, # Text to copy to clipboard
url: str | None = None, # URL to open
close: bool = False, # Close the launcher
hide: bool = False, # Hide the launcher
type_text: str | None = None, # Text to type (ydotool)
play_sound: str | None = None, # Sound to play
)
Convenience Methods¶
# Close the launcher
HamrPlugin.close()
# Copy text and close
HamrPlugin.copy_and_close(text: str)
# Launch desktop file and close
HamrPlugin.launch_and_close(desktop_file: str)
# Open URL (optionally close)
HamrPlugin.open_url(url: str, close: bool = True)
# Return error
HamrPlugin.error(message: str, details: str | None = None)
Action Handler Patterns¶
Primary Action¶
When action is None, the user pressed Enter on the item:
@plugin.on_action
def handle_action(item_id: str, action: str | None, context: str | None, source: str | None):
if action is None:
# Primary action (Enter pressed)
return HamrPlugin.copy_and_close(get_value(item_id))
elif action == "delete":
delete_item(item_id)
return build_results()
elif action == "edit":
return show_edit_form(item_id)
Ambient Actions¶
When source == "ambient", the action came from the ambient bar. Return status updates only (not results):
@plugin.on_action
async def handle_action(item_id: str, action: str | None, context: str | None, source: str | None):
if source == "ambient":
# Handle ambient bar action
perform_action(item_id, action)
# Update status but don't return results (would open plugin view)
await plugin.send_status(build_status())
return {}
# Regular action
return HamrPlugin.results([...])
Status Updates¶
Badges and Chips¶
await plugin.send_status({
"badges": [
{"text": "3", "color": "#4caf50"}, # Count badge
{"icon": "warning", "color": "orange"} # Icon badge
],
"chips": [
{"text": "Running", "icon": "timer"},
{"text": "2 active"}
]
})
Ambient Items¶
Persistent items shown in the ambient bar:
await plugin.send_status({
"ambient": [
{
"id": "timer:123",
"name": "Meeting",
"description": "05:30",
"icon": "timer",
"actions": [
{"id": "pause", "icon": "pause", "name": "Pause"},
{"id": "delete", "icon": "delete", "name": "Delete"}
]
}
]
})
FAB Override¶
Override the floating action button when launcher is closed:
await plugin.send_status({
"fab": {
"chips": [{"text": "05:30", "icon": "timer"}],
"showFab": True, # Force FAB visible
"priority": 10 # Higher priority wins
}
})
Indexing¶
Daemon plugins can emit index items for main search:
# On startup, emit full index
await plugin.send_index(items=[
{
"id": "item:1",
"name": "My Item",
"description": "Description",
"icon": "star",
"keywords": ["keyword1", "keyword2"],
"entryPoint": {
"step": "action",
"selected": {"id": "item:1"}
}
}
])
The entryPoint field allows items to skip directly to action step when selected from main search.
Debugging¶
Enable debug logging:
# Via environment variable
HAMR_PLUGIN_DEBUG=1 python3 handler.py
# Or in code
plugin = HamrPlugin(id="...", debug=True)
Debug messages go to stderr:
[my-plugin] Connecting to /run/user/1000/hamr.sock
[my-plugin] Connected
[my-plugin] Registered: {'ok': True}
[my-plugin] Received message: {'method': 'search', 'params': {...}}
Complete Example: Timer Plugin¶
#!/usr/bin/env python3
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from sdk.hamr_sdk import HamrPlugin
plugin = HamrPlugin(
id="timer",
name="Timer",
description="Countdown timers",
icon="timer"
)
timers = []
@plugin.on_initial
async def handle_initial(params=None):
return HamrPlugin.results(
get_timer_results(),
placeholder="Enter duration (e.g., 5m, 1h30m)..."
)
@plugin.on_search
async def handle_search(query: str, context: str | None):
return HamrPlugin.results(
get_timer_results(query),
input_mode="realtime"
)
@plugin.on_action
async def handle_action(item_id: str, action: str | None, context: str | None, source: str | None):
if source == "ambient":
# Ambient bar action - only send status update
handle_timer_action(item_id, action)
await plugin.send_status(get_status())
return {}
# Regular action
handle_timer_action(item_id, action)
return HamrPlugin.results(get_timer_results())
@plugin.add_background_task
async def tick_timers(p: HamrPlugin):
while True:
await asyncio.sleep(1)
update_timers()
await p.send_status(get_status())
plugin.run()
Manifest Configuration¶
For socket-based daemon plugins using the SDK, use this manifest structure:
{
"name": "My Plugin",
"description": "Plugin description",
"icon": "extension",
"handler": {
"type": "socket",
"command": "python3 handler.py"
},
"daemon": {
"enabled": true,
"background": true,
"restartOnCrash": true,
"maxRestarts": 5
},
"frecency": "plugin",
"supportedPlatforms": ["niri", "hyprland"]
}
Important: The SDK is for socket type handlers only. For simple stdio plugins, you don't need the SDK - just read JSON from stdin and print JSON to stdout.
See API Reference for full manifest documentation.
When to Use the SDK¶
| Use Case | SDK? | Handler Type |
|---|---|---|
| Simple stateless operations | No | stdio |
| Pattern matching (calculator) | No | stdio |
| Real-time updates (timers) | Yes | socket |
| Background monitoring (sound) | Yes | socket |
| Ambient items & FAB override | Yes | socket |
| Live sliders/switches | Yes | socket |