Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
docs/plugins/_archive/PLUGIN_LIFECYCLE_HOOKS.md
# Plugin Lifecycle and Hooks Reference
**Version**: v1.6.0
**Last Updated**: 2025-09-26
**Target**: Plugin developers
## Plugin Lifecycle Overview
Raxol plugins follow the `Raxol.Core.Runtime.Plugins.Plugin` behaviour and go through a well-defined lifecycle managed by the Plugin System v2.0.
## Core Plugin Behaviour
### Required Callbacks
All plugins must implement these callbacks:
```elixir
@behaviour Raxol.Core.Runtime.Plugins.Plugin
# Plugin initialization
@callback init(config()) :: {:ok, state()} | {:error, any()}
# Plugin termination
@callback terminate(any(), state()) :: any()
# Plugin enable/disable
@callback enable(state()) :: {:ok, state()} | {:error, any()}
@callback disable(state()) :: {:ok, state()} | {:error, any()}
```
### Optional Callbacks
These callbacks provide additional functionality:
```elixir
# Event filtering - intercept and modify system events
@callback filter_event(event(), state()) :: {:ok, event()} | :halt | any()
# Command handling - handle plugin-specific commands
@callback handle_command(command(), list(), state()) ::
{:ok, state(), any()} | {:error, any(), state()}
# Command declaration - list available commands
@callback get_commands() :: [{atom(), atom(), non_neg_integer()}]
```
## Detailed Lifecycle States
### 1. Discovery Phase
```
Plugin File/Module Discovery
↓
Manifest Validation
↓
Dependency Resolution
↓
Security Check
```
### 2. Loading Phase
```elixir
# Plugin System v2.0 loads plugin
{:ok, plugin_state} = YourPlugin.init(config)
# Plugin state transitions: :loaded
state = %PluginState{
manifest: manifest,
status: :loaded,
module: YourPlugin,
dependencies: resolved_deps,
performance_metrics: %{}
}
```
### 3. Starting Phase
```elixir
# System calls enable callback
{:ok, new_state} = YourPlugin.enable(plugin_state)
# Plugin state transitions: :starting -> :running
```
### 4. Runtime Phase
During runtime, plugins can:
#### Handle Events (Optional)
```elixir
def filter_event({:key_press, key}, state) do
case key do
"ctrl+p" ->
# Intercept Ctrl+P and open command palette
new_state = %{state | palette_open: true}
{:ok, {:plugin_event, :palette_open}}
_ ->
# Pass through other events
{:ok, {:key_press, key}}
end
end
```
#### Handle Commands (Optional)
```elixir
def handle_command(:show_status, [], state) do
status_info = generate_status_report(state)
{:ok, state, status_info}
end
def handle_command(:refresh_data, [source], state) do
new_data = fetch_data_from_source(source)
new_state = %{state | data: new_data}
{:ok, new_state, :refreshed}
end
# Declare available commands
def get_commands do
[
{:show_status, :handle_command, 3},
{:refresh_data, :handle_command, 3}
]
end
```
### 5. Stopping Phase
```elixir
# System calls disable callback
{:ok, final_state} = YourPlugin.disable(running_state)
# Plugin state transitions: :stopping -> :stopped
```
### 6. Termination Phase
```elixir
# System calls terminate callback
:ok = YourPlugin.terminate(:normal, final_state)
# Plugin removed from system
```
## Event System Integration
### Event Types
Plugins can filter and respond to these system events:
- **Input Events**: `{:key_press, key}`, `{:mouse_click, x, y}`, `{:scroll, direction}`
- **Terminal Events**: `{:terminal_resize, {width, height}}`, `{:terminal_focus, boolean}`
- **System Events**: `{:file_change, path}`, `{:process_exit, pid, reason}`
- **Plugin Events**: `{:plugin_loaded, name}`, `{:plugin_unloaded, name}`
### Event Flow
```
System Event
↓
Plugin.filter_event/2 (each loaded plugin)
↓
Modified/Original Event
↓
Core System Processing
```
### Event Filtering Examples
```elixir
# Block specific events
def filter_event({:key_press, "F12"}, state) do
# Don't let F12 reach the system
:halt
end
# Modify events
def filter_event({:key_press, key}, state) when key in ["j", "k", "h", "l"] do
# Convert vim keys to arrow keys
arrow_key = vim_to_arrow(key)
{:ok, {:key_press, arrow_key}}
end
# Pass through unchanged
def filter_event(event, _state) do
{:ok, event}
end
```
## Command Integration
### Command Registration
Commands are automatically registered when plugins are loaded:
```elixir
# In your plugin
def get_commands do
[
{:search, :handle_search_command, 3},
{:open_file, :handle_open_file_command, 3},
{:toggle_panel, :handle_toggle_panel_command, 3}
]
end
# Commands are accessible as:
# "plugin-name:search"
# "plugin-name:open_file"
# "plugin-name:toggle_panel"
```
### Command Execution
```elixir
# User executes: "my-plugin:search query term"
def handle_command(:search, ["query", "term"], state) do
results = perform_search(state, "query term")
new_state = %{state | search_results: results}
{:ok, new_state, results}
end
# Error handling
def handle_command(:invalid_command, _args, state) do
{:error, "Unknown command", state}
end
```
## State Management Patterns
### Plugin State Structure
```elixir
defmodule MyPlugin do
defstruct [
# Configuration from user
:config,
# UI state
:ui_visible,
:selected_item,
:cursor_position,
# Data state
:cached_data,
:last_update,
# Resource management
:timers,
:subscriptions,
:file_watchers
]
def init(config) do
state = %__MODULE__{
config: config,
ui_visible: false,
selected_item: 0,
cursor_position: {0, 0},
cached_data: %{},
last_update: DateTime.utc_now(),
timers: [],
subscriptions: [],
file_watchers: []
}
{:ok, state}
end
end
```
### State Transitions
```elixir
# Clean state transitions
def enable(%{config: config} = state) do
# Start timers
refresh_timer = :timer.send_interval(config.refresh_ms, self(), :refresh)
# Subscribe to events
:ok = subscribe_to_file_changes(config.watch_path)
new_state = %{state |
timers: [refresh_timer],
subscriptions: [:file_changes]
}
{:ok, new_state}
end
def disable(state) do
# Clean up resources
Enum.each(state.timers, &:timer.cancel/1)
unsubscribe_from_all(state.subscriptions)
clean_state = %{state |
timers: [],
subscriptions: []
}
{:ok, clean_state}
end
```
## Hot Reload Support
### State Migration
```elixir
# Optional callback for state migration during hot reload
def migrate_state(old_state, old_version, new_version) do
case {old_version, new_version} do
{"1.0.0", "1.1.0"} ->
# Add new field with default
Map.put(old_state, :new_field, "default_value")
{"1.1.0", "2.0.0"} ->
# Major version change - restructure state
%{
config: old_state.config,
ui_state: %{visible: old_state.ui_visible},
data: old_state.cached_data
}
_ ->
# No migration needed
old_state
end
end
```
### Resource Preservation
```elixir
def prepare_for_reload(state) do
# Save important data that should survive reload
persistent_data = %{
user_selections: state.selected_items,
cached_results: state.search_cache,
session_data: state.session_info
}
# Store in process dictionary or external storage
Process.put(:plugin_persistent_data, persistent_data)
{:ok, state}
end
def restore_after_reload(state) do
case Process.get(:plugin_persistent_data) do
nil ->
state
persistent_data ->
Process.delete(:plugin_persistent_data)
%{state |
selected_items: persistent_data.user_selections,
search_cache: persistent_data.cached_results,
session_info: persistent_data.session_data
}
end
end
```
## Plugin System Hooks
### System-Level Hooks
These hooks are called by the Plugin System v2.0:
- **pre_load**: Called before plugin is loaded
- **post_load**: Called after successful plugin load
- **pre_enable**: Called before plugin is enabled
- **post_enable**: Called after plugin is enabled
- **pre_disable**: Called before plugin is disabled
- **post_disable**: Called after plugin is disabled
- **pre_unload**: Called before plugin is unloaded
- **error_occurred**: Called when plugin encounters error
### Hook Implementation
```elixir
# In Plugin System v2.0 configuration
hooks: %{
pre_load: [&validate_system_requirements/1],
post_load: [®ister_plugin_commands/1],
error_occurred: [&log_plugin_error/2, ¬ify_administrators/2]
}
def validate_system_requirements(plugin_manifest) do
case check_dependencies(plugin_manifest.dependencies) do
:ok -> :continue
{:error, missing} -> {:halt, "Missing dependencies: #{inspect(missing)}"}
end
end
```
## Error Handling Patterns
### Graceful Degradation
```elixir
def handle_command(:complex_operation, args, state) do
case safe_complex_operation(args, state) do
{:ok, result, new_state} ->
{:ok, new_state, result}
{:error, :network_timeout} ->
# Fallback to cached data
cached_result = get_cached_result(args, state)
{:ok, state, cached_result}
{:error, reason} ->
Logger.warning("Operation failed: #{reason}")
{:error, "Operation temporarily unavailable", state}
end
end
defp safe_complex_operation(args, state) do
try do
result = perform_network_operation(args)
new_state = update_cache(state, args, result)
{:ok, result, new_state}
rescue
e in RuntimeError ->
{:error, e.message}
catch
:exit, reason ->
{:error, {:process_exit, reason}}
end
end
```
### Resource Cleanup
```elixir
def terminate(reason, state) do
Logger.info("Plugin terminating: #{reason}")
# Cancel all timers
state.timers
|> Enum.each(&:timer.cancel/1)
# Close files
state.open_files
|> Enum.each(&File.close/1)
# Cleanup network connections
state.connections
|> Enum.each(&HTTPoison.close/1)
# Save important state
save_plugin_state(state)
:ok
end
```
## Performance Considerations
### Efficient Event Handling
```elixir
# Fast event filtering with guards
def filter_event({:key_press, key}, state)
when key in ["j", "k", "h", "l"] do
# Quick vim key handling
{:ok, {:key_press, vim_to_arrow(key)}}
end
def filter_event({:key_press, _key}, _state) do
# Pass through other keys without processing
{:ok, event}
end
# Avoid expensive operations in event filters
def filter_event({:file_change, path}, state) do
# Don't process file immediately - schedule async processing
send(self(), {:process_file_change, path})
{:ok, {:file_change, path}}
end
```
### State Management Performance
```elixir
# Use efficient data structures
defstruct [
# Use maps for lookups
:items_by_id, # %{id => item}
# Use lists for ordered data
:ordered_items, # [item1, item2, ...]
# Cache expensive computations
:cached_results, # %{query => result}
:cache_timestamps # %{query => timestamp}
]
# Batch updates
def update_multiple_items(state, updates) do
new_items_by_id =
updates
|> Enum.reduce(state.items_by_id, fn {id, update}, acc ->
Map.update(acc, id, update, &merge_update(&1, update))
end)
%{state | items_by_id: new_items_by_id}
end
```
## Testing Plugin Lifecycle
### Lifecycle Test Example
```elixir
defmodule MyPluginLifecycleTest do
use ExUnit.Case
alias MyPlugin
describe "plugin lifecycle" do
test "complete lifecycle works" do
config = %{enabled: true}
# Test initialization
assert {:ok, state} = MyPlugin.init(config)
assert state.config == config
# Test enable
assert {:ok, enabled_state} = MyPlugin.enable(state)
assert length(enabled_state.timers) > 0
# Test command handling
{:ok, new_state, result} = MyPlugin.handle_command(:test_cmd, [], enabled_state)
assert result == :expected
# Test disable
assert {:ok, disabled_state} = MyPlugin.disable(new_state)
assert disabled_state.timers == []
# Test termination
assert :ok = MyPlugin.terminate(:normal, disabled_state)
end
end
end
```
This comprehensive lifecycle and hooks reference provides plugin developers with deep understanding of how plugins interact with the Raxol system throughout their entire lifecycle.