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
raxol lib raxol plugins clipboard_plugin.ex
Raw

lib/raxol/plugins/clipboard_plugin.ex

defmodule Raxol.Plugins.ClipboardPlugin do
@moduledoc """
Plugin for clipboard operations in Raxol.
"""
@behaviour Raxol.Plugins.Plugin
require Raxol.Core.Runtime.Log
# Alias the new consolidated module
alias Raxol.System.Clipboard
@type t :: %__MODULE__{
# Standard Plugin fields
name: String.t(),
version: String.t(),
description: String.t(),
enabled: boolean(),
config: map(),
dependencies: list(map()),
api_version: String.t(),
# Clipboard-specific state
selection_active: boolean(),
# {x, y}
selection_start: {integer(), integer()} | nil,
# {x, y}
selection_end: {integer(), integer()} | nil,
last_cells_at_selection: map() | nil
}
defstruct [
# Standard fields
name: "clipboard",
version: "0.1.0",
description: "Handles clipboard copy and paste.",
enabled: true,
config: %{},
dependencies: [],
api_version: "1.0.1",
# Clipboard-specific state
selection_active: false,
selection_start: nil,
selection_end: nil,
last_cells_at_selection: nil
]
@impl Raxol.Plugins.Plugin
def init(config \\ %{}) do
# Initialize the plugin struct, merging provided config
plugin_state = struct(__MODULE__, config)
{:ok, plugin_state}
end
@impl Raxol.Plugins.Plugin
def handle_input(
%__MODULE__{} = state,
%{type: :key, modifiers: mods, key: ?c} = _event
) do
handle_copy_key(state, mods)
end
def handle_input(
%__MODULE__{} = state,
%{type: :key, modifiers: mods, key: ?v} = _event
) do
handle_paste_key(state, mods)
end
# Catch-all for other map events or non-map events
def handle_input(state, _event), do: {:ok, state}
defp yank_selection(%__MODULE__{} = state) do
case get_selected_text(state) do
{:ok, text} ->
# Call the new consolidated module
case Clipboard.copy(text) do
:ok ->
Raxol.Core.Runtime.Log.debug(
"[Clipboard] Yanked selection to clipboard: #{text}"
)
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"[Clipboard] Failed to yank selection: #{inspect(reason)}"
)
end
{:ok, state}
_ ->
{:ok, state}
end
end
defp get_selected_text(%__MODULE__{} = state) do
case state do
%{
selection_start: start_pos,
selection_end: end_pos,
last_cells_at_selection: cells
} ->
process_valid_selection(
valid_selection?(start_pos, end_pos, cells),
start_pos,
end_pos,
cells
)
_ ->
{:error, :no_selection}
end
end
defp valid_selection?(start_pos, end_pos, cells) do
is_tuple(start_pos) and tuple_size(start_pos) == 2 and
is_tuple(end_pos) and tuple_size(end_pos) == 2 and
is_map(cells)
end
defp get_selection_bounds({sx, sy}, {ex, ey}) do
{min(sx, ex), max(sx, ex), min(sy, ey), max(sy, ey)}
end
defp build_selected_text(cells, min_x, max_x, min_y, max_y) do
for y <- min_y..max_y do
process_line(cells, y, min_x, max_x)
end
|> Enum.join("\n")
end
defp process_line(cells, y, min_x, max_x) do
for x <- min_x..max_x do
process_cell(Map.get(cells, {x, y}))
end
|> Enum.reject(&is_nil/1)
|> Enum.join()
end
defp process_cell(%{char: char}) when is_integer(char), do: <<char::utf8>>
defp process_cell(_), do: nil
defp clear_selection(state) do
%{state | selection_active: false, selection_start: nil, selection_end: nil}
end
# Reads content from the system clipboard using the consolidated module.
defp get_clipboard_content() do
# Delegate directly to the new module
Clipboard.paste()
end
@impl Raxol.Plugins.Plugin
def get_dependencies, do: []
@impl Raxol.Plugins.Plugin
def get_api_version, do: "1.0.1"
@doc """
Returns the API version for compatibility checking.
"""
def api_version do
"1.0.0"
end
defp handle_copy_key(state, mods) do
handle_ctrl_modifier(state, :ctrl in mods, :copy)
end
defp handle_paste_key(state, mods) do
handle_ctrl_modifier(state, :ctrl in mods, :paste)
end
defp handle_ctrl_modifier(state, false, _action) do
{:ok, state}
end
defp handle_ctrl_modifier(state, true, :copy) do
Raxol.Core.Runtime.Log.debug("[Clipboard] Ctrl+C detected.")
# Check for finalized selection and stored cells
handle_copy_selection(state, has_complete_selection?(state))
end
defp handle_ctrl_modifier(state, true, :paste) do
Raxol.Core.Runtime.Log.debug("[Clipboard] Ctrl+V detected.")
case get_clipboard_content() do
{:ok, content} when content != "" ->
Raxol.Core.Runtime.Log.debug("[Clipboard] Pasting content: #{content}")
# Return command for Runtime to handle
{:ok, state, {:command, {:paste, content}}}
{:ok, ""} ->
Raxol.Core.Runtime.Log.debug(
"[Clipboard] Clipboard is empty, nothing to paste."
)
{:ok, state}
{:error, reason} ->
Raxol.Core.Runtime.Log.error(
"[Clipboard] Failed to get clipboard content: #{inspect(reason)}"
)
{:ok, state}
end
end
defp has_complete_selection?(state) do
is_tuple(state.selection_start) and is_tuple(state.selection_end) and
is_map(state.last_cells_at_selection)
end
defp handle_copy_selection(state, true) do
Raxol.Core.Runtime.Log.debug("[Clipboard] Triggering yank_selection.")
result = yank_selection(state)
new_state = clear_selection(state)
# Store the yank result for debugging if needed
new_state = Map.put(new_state, :last_yank_result, result)
{:ok, new_state}
end
defp handle_copy_selection(state, false) do
Raxol.Core.Runtime.Log.debug(
"[Clipboard] No complete selection available for copy."
)
{:ok, state}
end
defp process_valid_selection(true, start_pos, end_pos, cells) do
{min_x, max_x, min_y, max_y} = get_selection_bounds(start_pos, end_pos)
{:ok, build_selected_text(cells, min_x, max_x, min_y, max_y)}
end
defp process_valid_selection(false, _start_pos, _end_pos, _cells) do
{:error, :no_selection}
end
end