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
lib/raxol/plugins/plugin_manager.ex
defmodule Raxol.Plugins.PluginManager do
@moduledoc """
Manages plugins for the Raxol terminal emulator.
Handles plugin loading, lifecycle management, and event dispatching.
"""
require Logger
alias Raxol.Plugins.{Plugin, PluginConfig, PluginDependency}
@type t :: %__MODULE__{
plugins: %{String.t() => Plugin.t()},
config: PluginConfig.t(),
api_version: String.t()
}
defstruct [
:plugins,
:config,
:api_version
]
@doc """
Creates a new plugin manager with default configuration.
"""
def new(_config \\ %{}) do
# Initialize with a default PluginConfig
initial_config = PluginConfig.new()
%__MODULE__{
plugins: %{},
config: initial_config,
# Set a default API version
api_version: "1.0"
}
end
@doc """
Loads a plugin module and initializes it with the given configuration.
"""
def load_plugin(%__MODULE__{} = manager, module, config \\ %{})
when is_atom(module) do
# Get persisted config for this plugin
plugin_name =
Atom.to_string(module)
|> String.split(".")
|> List.last()
|> Macro.underscore()
persisted_config =
PluginConfig.get_plugin_config(manager.config, plugin_name)
# Merge persisted config with provided config
merged_config = Map.merge(persisted_config, config)
case module.init(merged_config) do
{:ok, plugin} ->
# Check API compatibility
case PluginDependency.check_api_compatibility(
plugin.api_version,
manager.api_version
) do
:ok ->
# Check dependencies
case PluginDependency.check_dependencies(
plugin,
list_plugins(manager)
) do
{:ok, _} ->
# Update plugin config with merged config
updated_config =
PluginConfig.update_plugin_config(
manager.config,
plugin_name,
merged_config
)
# Save updated config
case PluginConfig.save(updated_config) do
{:ok, saved_config} ->
{:ok,
%{
manager
| plugins: Map.put(manager.plugins, plugin.name, plugin),
config: saved_config
}}
{:error, _reason} ->
# Continue even if save fails, return manager with plugin loaded
{:ok,
%{
manager
| plugins: Map.put(manager.plugins, plugin.name, plugin)
# Maybe log the config save error here?
}}
end
{:error, reason} ->
{:error, "Failed to load plugin #{module}: #{reason}"}
end
{:error, reason} ->
{:error, "Failed to load plugin #{module}: #{reason}"}
end
{:error, reason} ->
{:error, "Failed to load plugin #{module}: #{reason}"}
end
end
@doc """
Loads multiple plugins in the correct dependency order.
"""
def load_plugins(%__MODULE__{} = manager, modules) when is_list(modules) do
# First, initialize all plugins without adding them to the manager
plugins =
Enum.reduce_while(modules, {:ok, []}, fn module, {:ok, acc_plugins} ->
plugin_name =
Atom.to_string(module)
|> String.split(".")
|> List.last()
|> Macro.underscore()
persisted_config =
PluginConfig.get_plugin_config(manager.config, plugin_name)
case module.init(persisted_config) do
{:ok, plugin} ->
{:cont, {:ok, [plugin | acc_plugins]}}
{:error, reason} ->
{:halt,
{:error, "Failed to initialize plugin #{module}: #{reason}"}}
end
end)
case plugins do
{:ok, initialized_plugins} ->
# Resolve dependencies to get the correct load order
case PluginDependency.resolve_dependencies(initialized_plugins) do
{:ok, sorted_plugin_names} ->
# Load plugins in the correct order
Enum.reduce_while(
sorted_plugin_names,
{:ok, manager},
fn plugin_name, {:ok, acc_manager} ->
# Find the plugin in initialized plugins
case Enum.find(initialized_plugins, fn p ->
p.name == plugin_name
end) do
nil ->
{:halt,
{:error,
"Plugin #{plugin_name} not found in initialized plugins"}}
_plugin ->
# Load the plugin using its module name
case load_plugin(
acc_manager,
String.to_atom(
"Elixir.#{String.replace(plugin_name, "_", ".")}"
)
) do
# Match on {:ok, updated_manager} now
{:ok, updated_manager} ->
{:cont, {:ok, updated_manager}}
# Handle the direct manager return case (if any remain after fixing load_plugin/2)
updated_manager when is_map(updated_manager) ->
Logger.warning(
"load_plugin returned manager directly, expected {:ok, manager}",
plugin: plugin_name
)
{:cont, {:ok, updated_manager}}
{:error, reason} ->
{:halt,
{:error,
"Failed to load plugin #{plugin_name}: #{reason}"}}
end
end
end
)
{:error, reason} ->
{:error, "Failed to resolve plugin dependencies: #{reason}"}
end
{:error, _reason} = error ->
error
end
end
@doc """
Unloads a plugin by name.
"""
def unload_plugin(%__MODULE__{} = manager, name) when is_binary(name) do
case Map.get(manager.plugins, name) do
nil ->
{:error, "Plugin #{name} not found"}
plugin ->
# Get module from struct BEFORE calling cleanup
module = plugin.__struct__
# Call cleanup on the module, passing the plugin state
case module.cleanup(plugin) do
:ok ->
# Update config to disable plugin
updated_config = PluginConfig.disable_plugin(manager.config, name)
# Save updated config
case PluginConfig.save(updated_config) do
{:ok, saved_config} ->
# Return {:ok, updated_manager}
{:ok,
%{
manager
| plugins: Map.delete(manager.plugins, name),
config: saved_config
}}
{:error, _reason} ->
# Continue even if save fails, but still return {:ok, updated_manager}
{:ok, %{manager | plugins: Map.delete(manager.plugins, name)}}
end
{:error, reason} ->
{:error, "Failed to cleanup plugin #{name}: #{reason}"}
end
end
end
@doc """
Enables a plugin by name.
"""
def enable_plugin(%__MODULE__{} = manager, name) when is_binary(name) do
case Map.get(manager.plugins, name) do
nil ->
{:error, "Plugin #{name} not found"}
plugin ->
# Check dependencies before enabling
case PluginDependency.check_dependencies(plugin, list_plugins(manager)) do
{:ok, _} ->
# Update config to enable plugin
updated_config = PluginConfig.enable_plugin(manager.config, name)
# Save updated config
case PluginConfig.save(updated_config) do
{:ok, saved_config} ->
%{
manager
| plugins:
Map.put(manager.plugins, plugin.name, %{
plugin
| enabled: true
}),
config: saved_config
}
{:error, _reason} ->
# Continue even if save fails
%{
manager
| plugins:
Map.put(manager.plugins, plugin.name, %{
plugin
| enabled: true
})
}
end
{:error, reason} ->
{:error, "Cannot enable plugin #{name}: #{reason}"}
end
end
end
@doc """
Disables a plugin by name.
"""
def disable_plugin(%__MODULE__{} = manager, name) when is_binary(name) do
case Map.get(manager.plugins, name) do
nil ->
{:error, "Plugin #{name} not found"}
plugin ->
# Update config to disable plugin
updated_config = PluginConfig.disable_plugin(manager.config, name)
# Save updated config
case PluginConfig.save(updated_config) do
{:ok, saved_config} ->
%{
manager
| plugins:
Map.put(manager.plugins, plugin.name, %{
plugin
| enabled: false
}),
config: saved_config
}
{:error, _reason} ->
# Continue even if save fails
%{
manager
| plugins:
Map.put(manager.plugins, plugin.name, %{
plugin
| enabled: false
})
}
end
end
end
@doc """
Processes input through all enabled plugins.
"""
def process_input(%__MODULE__{} = manager, input) when is_binary(input) do
Enum.reduce_while(manager.plugins, {:ok, manager}, fn {_name, plugin},
{:ok, acc_manager} ->
if plugin.enabled do
case plugin.handle_input(input) do
{:ok, updated_plugin} ->
{:cont,
{:ok,
%{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}}}
{:error, reason} ->
{:halt,
{:error, "Plugin #{plugin.name} failed to handle input: #{reason}"}}
end
else
{:cont, {:ok, acc_manager}}
end
end)
end
@doc """
Processes output through all enabled plugins.
Returns {:ok, manager, transformed_output} if a plugin transforms the output,
or {:ok, manager} if no transformation is needed.
"""
def process_output(%__MODULE__{} = manager, output) when is_binary(output) do
Enum.reduce_while(manager.plugins, {:ok, manager, output}, fn {_name,
plugin},
{:ok,
acc_manager,
acc_output} ->
if plugin.enabled do
# Get module from struct BEFORE calling handle_output
module = plugin.__struct__
# Call handle_output on the module, passing plugin state and output
case module.handle_output(plugin, output) do
{:ok, updated_plugin} ->
{:cont,
{:ok,
%{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}, acc_output}}
{:ok, updated_plugin, transformed_output} ->
{:cont,
{:ok,
%{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}, transformed_output}}
{:error, reason} ->
{:halt,
{:error,
"Plugin #{plugin.name} failed to handle output: #{reason}"}}
end
else
{:cont, {:ok, acc_manager, acc_output}}
end
end)
end
@doc """
Processes mouse events through all enabled plugins.
"""
def process_mouse(%__MODULE__{} = manager, event, emulator_state)
when is_tuple(event) do
Enum.reduce_while(manager.plugins, {:ok, manager}, fn {_name, plugin},
{:ok, acc_manager} ->
if plugin.enabled do
# Get module from struct BEFORE calling handle_mouse
module = plugin.__struct__
# Check if module implements handle_mouse/3
if function_exported?(module, :handle_mouse, 3) do
# Call handle_mouse on the module, passing plugin state, event, and emulator_state
case module.handle_mouse(plugin, event, emulator_state) do
{:ok, updated_plugin} ->
{:cont,
{:ok,
%{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}}}
{:error, reason} ->
{:halt,
{:error,
"Plugin #{plugin.name} failed to handle mouse event: #{reason}"}}
end
else
# Plugin disabled or doesn't implement handle_mouse/3, continue
{:cont, {:ok, acc_manager}}
end
else
{:cont, {:ok, acc_manager}}
end
end)
end
@doc """
Notifies all enabled plugins of a terminal resize event.
"""
def handle_resize(%__MODULE__{} = manager, width, height)
when is_integer(width) and is_integer(height) do
Enum.reduce_while(manager.plugins, {:ok, manager}, fn {_name, plugin},
{:ok, acc_manager} ->
if plugin.enabled do
# Get module from struct BEFORE calling handle_resize
module = plugin.__struct__
# Check if module implements handle_resize/3
if function_exported?(module, :handle_resize, 3) do
# Call handle_resize on the module, passing plugin state, width, and height
case module.handle_resize(plugin, width, height) do
{:ok, updated_plugin} ->
{:cont,
{:ok,
%{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}}}
{:error, reason} ->
{:halt,
{:error,
"Plugin #{plugin.name} failed to handle resize: #{reason}"}}
end
else
# Plugin disabled or doesn't implement handle_resize/3, continue
{:cont, {:ok, acc_manager}}
end
else
{:cont, {:ok, acc_manager}}
end
end)
end
@doc """
Gets a list of all loaded plugins.
"""
def list_plugins(%__MODULE__{} = manager) do
Map.values(manager.plugins)
end
@doc """
Gets a plugin by name.
"""
def get_plugin(%__MODULE__{} = manager, name) when is_binary(name) do
Map.get(manager.plugins, name)
end
@doc """
Runs render-related hooks for all enabled plugins.
Collects any direct output commands (e.g., escape sequences) returned by plugins.
Returns {:ok, updated_manager, list_of_output_commands}
"""
def run_render_hooks(%__MODULE__{} = manager) do
Enum.reduce(manager.plugins, {:ok, manager, []}, fn {_name, plugin},
{:ok, acc_manager,
acc_commands} ->
if plugin.enabled do
# Get the module from the struct
module = plugin.__struct__
# Check if module implements handle_render
if function_exported?(module, :handle_render, 1) do
# Call using the module with plugin state as first argument
case module.handle_render(plugin) do
{:ok, updated_plugin, command} when not is_nil(command) ->
updated_manager = %{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}
{:ok, updated_manager, [command | acc_commands]}
# No command returned
{:ok, updated_plugin} ->
updated_manager = %{
acc_manager
| plugins:
Map.put(acc_manager.plugins, plugin.name, updated_plugin)
}
{:ok, updated_manager, acc_commands}
# Allow plugins to just return the command if state doesn't change
command when is_binary(command) ->
{:ok, acc_manager, [command | acc_commands]}
# Ignore other return values or errors for now
_ ->
{:ok, acc_manager, acc_commands}
end
else
# Plugin doesn't implement hook
{:ok, acc_manager, acc_commands}
end
else
# Plugin disabled
{:ok, acc_manager, acc_commands}
end
end)
end
@doc """
Gets the current API version of the plugin manager.
"""
def get_api_version(%__MODULE__{} = manager) do
manager.api_version
end
@doc """
Updates the state of a specific plugin within the manager.
The `update_fun` receives the current plugin state and should return the new state.
"""
def update_plugin(%__MODULE__{} = manager, name, update_fun)
when is_binary(name) and is_function(update_fun, 1) do
case Map.get(manager.plugins, name) do
nil ->
{:error, "Plugin #{name} not found"}
plugin ->
try do
new_plugin_state = update_fun.(plugin)
# Basic validation: ensure it's still the same struct type
if is_struct(new_plugin_state, plugin.__struct__) do
updated_manager = %{
manager
| plugins: Map.put(manager.plugins, name, new_plugin_state)
}
{:ok, updated_manager}
else
{:error,
"Update function returned invalid state for plugin #{name}"}
end
rescue
e -> {:error, "Error updating plugin #{name}: #{inspect(e)}"}
end
end
end
@doc """
Processes a mouse event through all enabled plugins, providing cell context.
Plugins can choose to halt propagation if they handle the event.
Returns {:ok, updated_manager, :propagate | :halt} or {:error, reason}.
"""
def handle_mouse_event(%__MODULE__{} = manager, event, rendered_cells)
when is_map(event) do
# Reduce over enabled plugins, stopping if one halts
Enum.reduce_while(manager.plugins, {:ok, manager, :propagate}, fn
{_name, plugin}, {:ok, acc_manager, _propagation_state} ->
if plugin.enabled and
function_exported?(plugin.__struct__, :handle_mouse, 3) do
# Pass the current plugin state, the event map, and the rendered cell context
current_plugin_state = Map.get(acc_manager.plugins, plugin.name)
module = plugin.__struct__
case module.handle_mouse(current_plugin_state, event, rendered_cells) do
{:ok, updated_plugin_state, :propagate} ->
new_manager_state = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin.name,
updated_plugin_state
)
}
# Continue processing other plugins
{:cont, {:ok, new_manager_state, :propagate}}
{:ok, updated_plugin_state, :halt} ->
new_manager_state = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin.name,
updated_plugin_state
)
}
# Halt processing, this plugin handled it
{:halt, {:ok, new_manager_state, :halt}}
{:error, reason} ->
Logger.error(
"Error from plugin #{plugin.name} in handle_mouse: #{inspect(reason)}"
)
# Halt with an error
{:halt, {:error, reason}}
# Invalid return from plugin
_ ->
Logger.warning(
"Invalid return from #{plugin.name}.handle_mouse/3. Propagating."
)
# Continue, assuming propagate
{:cont, {:ok, acc_manager, :propagate}}
end
else
# Plugin disabled or doesn't implement handle_mouse/3, continue
{:cont, {:ok, acc_manager, :propagate}}
end
end)
end
@doc """
Allows plugins to process or replace cells generated by the renderer.
Iterates through enabled plugins that implement `handle_cells/3`.
The callback should return `{:ok, updated_plugin_state, cells_to_render, commands}`.
Returns `{:ok, updated_manager, processed_cells, collected_commands}`.
"""
def handle_cells(%__MODULE__{} = manager, cells, emulator_state)
when is_list(cells) do
Logger.debug(
"[PluginManager.handle_cells] Processing #{length(cells)} cells..."
)
# Accumulator: {updated_manager, processed_cells_list_reversed, collected_commands_list}
initial_acc = {manager, [], []}
{final_manager, final_cells_rev, final_commands} =
Enum.reduce(cells, initial_acc, fn cell,
{acc_manager, processed_cells_rev,
acc_commands} ->
# Check if the cell is a placeholder potentially handled by a plugin
case cell do
%{type: :placeholder, value: placeholder_value} = placeholder_cell ->
Logger.debug(
"[PluginManager.handle_cells] Found placeholder: #{inspect(placeholder_value)}"
)
# Find plugins that might handle this placeholder type
# Inner accumulator: {handled_flag, manager_state, list_of_replacement_cells, list_of_new_commands}
# Default replacement_cells to an empty list, signifying removal if not handled.
# Start inner loop with current outer loop manager state
inner_initial_acc = {false, acc_manager, [], []}
# Result of inner loop: {handled_flag, manager_state_after_inner_loop, replacement_cells_list, new_commands_list}
# Find the specific plugin based on the placeholder value
plugin_name =
case placeholder_value do
:image -> "image"
:chart -> "visualization"
:treemap -> "visualization"
# Unknown placeholder type
_ -> nil
end
{_plugin_handled, manager_after_inner_loop, replacement_cells,
new_commands} =
if plugin_name do
# Get the specific plugin state
case Map.get(acc_manager.plugins, plugin_name) do
nil ->
Logger.warning(
"[PluginManager.handle_cells] Plugin '#{plugin_name}' not loaded for placeholder '#{placeholder_value}'. Skipping."
)
# Return default accumulator if plugin not found
inner_initial_acc
plugin ->
# Call only the relevant plugin's handle_cells
if plugin.enabled and
function_exported?(plugin.__struct__, :handle_cells, 3) do
# Log state *before* calling plugin, especially for ImagePlugin
if plugin_name == "image" do
Logger.debug(
"[PluginManager.handle_cells] Before calling ImagePlugin.handle_cells. sequence_just_generated: #{inspect(Map.get(plugin, :sequence_just_generated))}"
)
end
# Log the opts specifically
Logger.debug(
"[PluginManager.handle_cells] Placeholder opts: #{inspect(Map.get(placeholder_cell, :opts))}"
)
Logger.debug(
"[PluginManager.handle_cells] Calling #{plugin_name}.handle_cells for placeholder...\nCELL DATA: #{inspect(placeholder_cell)}"
)
try do
# Assign result to variable first
handle_cells_result =
plugin.__struct__.handle_cells(
placeholder_cell,
emulator_state,
plugin
)
# Now match on the result variable
case handle_cells_result do
# Plugin handled it, returning cells and commands
{:ok, updated_plugin_state, plugin_cells,
plugin_commands}
when is_list(plugin_cells) ->
if plugin_name == "image" do
Logger.debug(
"[PluginManager.handle_cells] After ImagePlugin.handle_cells returned {:ok, ...}. sequence_just_generated: #{inspect(Map.get(updated_plugin_state, :sequence_just_generated))}"
)
end
Logger.debug(
"[PluginManager.handle_cells] Plugin #{plugin_name} handled placeholder. Cells: #{length(plugin_cells)}, Commands: #{length(plugin_commands)}"
)
# Update manager state
updated_inner_manager = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin_name,
updated_plugin_state
)
}
# Return the result for the outer loop (handled = true)
{true, updated_inner_manager, plugin_cells,
plugin_commands}
# Plugin declined or returned unexpected success format
{:ok, updated_plugin_state, _invalid_cells,
plugin_commands} ->
Logger.warning(
"[PluginManager.handle_cells] Plugin #{plugin_name} handled placeholder but returned invalid cell format. Treating as decline."
)
# Update manager state
updated_inner_manager = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin_name,
updated_plugin_state
)
}
# Return default accumulator (handled = false), but with updated manager and commands
{false, updated_inner_manager, [], plugin_commands}
# Handle cases where plugin declines (:cont)
{:cont, updated_plugin_state} ->
Logger.debug(
"[PluginManager.handle_cells] Plugin #{plugin_name} returned :cont. State Flag: #{inspect(Map.get(updated_plugin_state, :sequence_just_generated))}"
)
if plugin_name == "image" do
Logger.debug(
"[PluginManager.handle_cells] After ImagePlugin.handle_cells returned {:cont, ...}. sequence_just_generated: #{inspect(Map.get(updated_plugin_state, :sequence_just_generated))}"
)
end
Logger.debug(
"[PluginManager.handle_cells] Plugin #{plugin_name} declined placeholder."
)
# Update manager state
updated_inner_manager = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin_name,
updated_plugin_state
)
}
# Return default accumulator (handled = false), but with updated manager
{false, updated_inner_manager, [], []}
# {:error, _} or other unexpected return
_ ->
Logger.warning(
"[PluginManager.handle_cells] Plugin #{plugin_name} returned unexpected value from handle_cells. Skipping."
)
# Return default accumulator
inner_initial_acc
end
rescue
e ->
Logger.error(
"[PluginManager.handle_cells] RESCUED Error calling #{plugin_name}.handle_cells: #{inspect(e)}. Placeholder was: #{inspect(placeholder_cell)}"
)
# Return default accumulator
inner_initial_acc
end
# End try/rescue
else
# Plugin exists but is disabled or doesn't implement handle_cells
Logger.debug(
"[PluginManager.handle_cells] Plugin '#{plugin_name}' disabled or does not implement handle_cells/3. Skipping."
)
inner_initial_acc
end
# End if plugin enabled/implements
end
# End case Map.get plugin
else
# No plugin name determined for this placeholder value
Logger.warning(
"[PluginManager.handle_cells] Unknown placeholder value: #{placeholder_value}. Skipping."
)
inner_initial_acc
end
# End if plugin_name
# Return the accumulator for the NEXT outer loop iteration.
# Use the manager state resulting from the inner processing.
{manager_after_inner_loop, replacement_cells ++ processed_cells_rev,
acc_commands ++ new_commands}
# Original cell was not a placeholder, assume it's {x, y, map}
valid_cell ->
# Prepend to the reversed list, pass original acc_manager forward
{acc_manager, [valid_cell | processed_cells_rev], acc_commands}
end
end)
# End of outer Enum.reduce
final_cells = Enum.reverse(final_cells_rev)
Logger.debug(
"[PluginManager.handle_cells] Finished. Final Cells: #{length(final_cells)}, Commands: #{length(final_commands)}"
)
# Return the final manager state accumulated through the outer loop
{:ok, final_manager, final_cells, final_commands}
end
@doc """
Processes a keyboard event through all enabled plugins.
Plugins can return commands and choose to halt propagation.
Returns {:ok, updated_manager, list_of_commands, :propagate | :halt} or {:error, reason}.
"""
def handle_key_event(%__MODULE__{} = manager, event, _rendered_cells)
when is_map(event) and event.type == :key do
# Reduce over enabled plugins, collecting commands and stopping if one halts
# {status, manager, commands, propagation}
initial_acc = {:ok, manager, [], :propagate}
Enum.reduce_while(manager.plugins, initial_acc, fn
{_name, plugin}, {:ok, acc_manager, acc_commands, _propagation_state} ->
if plugin.enabled and
function_exported?(plugin.__struct__, :handle_input, 2) do
# Pass the current plugin state and the event map
# NOTE: We are passing the *event map* to handle_input, which expects a string.
# This is a temporary adaptation. Ideally, a new callback like handle_key_event
# should be defined in the Plugin behaviour.
current_plugin_state = Map.get(acc_manager.plugins, plugin.name)
module = plugin.__struct__
case module.handle_input(current_plugin_state, event) do
# Plugin returns {:ok, state, command}
{:ok, updated_plugin_state, {:command, command_data}} ->
new_manager_state = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin.name,
updated_plugin_state
)
}
# Continue processing, add command, assume propagation (plugin didn't explicitly halt)
{:cont,
{:ok, new_manager_state, [command_data | acc_commands],
:propagate}}
# Plugin returns {:ok, state} (no command)
{:ok, updated_plugin_state} ->
new_manager_state = %{
acc_manager
| plugins:
Map.put(
acc_manager.plugins,
plugin.name,
updated_plugin_state
)
}
# Continue processing, no command added
{:cont, {:ok, new_manager_state, acc_commands, :propagate}}
# TODO: Add support for explicit :halt if needed
# {:ok, updated_plugin_state, :halt} -> ...
# {:ok, updated_plugin_state, {:command, cmd}, :halt} -> ...
{:error, reason} ->
Logger.error(
"Error from plugin #{plugin.name} in handle_input: #{inspect(reason)}"
)
# Halt with an error
{:halt, {:error, reason}}
# Invalid return from plugin
_ ->
Logger.warning(
"Invalid return from #{plugin.name}.handle_input/2. Propagating."
)
# Continue, assuming propagate
{:cont, {:ok, acc_manager, acc_commands, :propagate}}
end
else
# Plugin disabled or doesn't implement handle_input/2, continue
{:cont, {:ok, acc_manager, acc_commands, :propagate}}
end
end)
# Reverse commands to maintain order? Depends on processing logic.
|> case do
{:ok, final_manager, final_commands, propagation_state} ->
{:ok, final_manager, Enum.reverse(final_commands), propagation_state}
# Pass through {:error, reason}
error ->
error
end
end
end