Packages
Terminal emulation and driver infrastructure for Raxol. ANSI parsing, screen buffers, command processing, cursor management, input handling, session management, and termbox2 NIF integration.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/commands/osc_handler.ex
defmodule Raxol.Terminal.Commands.OSCHandler do
@moduledoc """
Consolidated OSC (Operating System Command) handler for terminal control sequences.
Combines all OSC handler functionality including window, clipboard, color, and selection operations.
"""
alias Raxol.Terminal.{Clipboard, Colors}
require Raxol.Core.Runtime.Log
# Alias for backward compatibility
def handle_osc_sequence(emulator, command, data) do
handle(emulator, command, data)
end
def handle(emulator, command, data) do
case get_command_group(command) do
{:window, cmd} ->
handle_window_ops(emulator, cmd, data)
{:clipboard, cmd} ->
handle_clipboard_ops(emulator, cmd, data)
{:color, cmd} ->
handle_color_ops(emulator, cmd, data)
{:cursor, cmd} ->
handle_cursor_ops(emulator, cmd, data)
{:standalone, cmd} ->
handle_standalone_ops(emulator, cmd, data)
:unsupported ->
Raxol.Core.Runtime.Log.warning("Unsupported OSC command: #{command}")
{:error, :unsupported_command, emulator}
end
end
defp get_command_group(command) do
command_groups = [
{[0, 1, 2, 7, 8, 1337], :window},
{[9, 52], :clipboard},
{[10, 11, 17, 19], :color},
{[12, 50, 112], :cursor},
{[4, 51], :standalone}
]
result =
Enum.find_value(command_groups, fn {commands, group} ->
case command in commands do
true -> {group, command}
false -> nil
end
end)
result || :unsupported
end
defp handle_standalone_ops(emulator, command, data) do
case command do
4 -> __MODULE__.ColorPalette.handle_4(emulator, data)
51 -> __MODULE__.Selection.handle_51(emulator, data)
end
end
defp handle_window_ops(emulator, command, data) do
case command do
0 -> __MODULE__.Window.handle_0(emulator, data)
1 -> __MODULE__.Window.handle_1(emulator, data)
2 -> __MODULE__.Window.handle_2(emulator, data)
7 -> __MODULE__.Window.handle_7(emulator, data)
8 -> __MODULE__.Window.handle_8(emulator, data)
1337 -> __MODULE__.Window.handle_1337(emulator, data)
end
end
defp handle_clipboard_ops(emulator, command, data) do
case command do
9 -> __MODULE__.Clipboard.handle_9(emulator, data)
52 -> __MODULE__.Clipboard.handle_52(emulator, data)
end
end
defp handle_color_ops(emulator, command, data) do
case command do
10 -> __MODULE__.Color.handle_10(emulator, data)
11 -> __MODULE__.Color.handle_11(emulator, data)
17 -> __MODULE__.Color.handle_17(emulator, data)
19 -> __MODULE__.Color.handle_19(emulator, data)
end
end
defp handle_cursor_ops(emulator, command, _data) do
case command do
# Set cursor color
12 -> {:ok, emulator}
# Set cursor shape
50 -> {:ok, emulator}
# Reset cursor color
112 -> {:ok, emulator}
_ -> {:ok, emulator}
end
end
def handle_window_title(emulator, data),
do: {:ok, %{emulator | window_title: data}}
def handle_icon_name(emulator, data),
do: {:ok, %{emulator | icon_name: data}}
# Clipboard sub-module
defmodule Clipboard do
@moduledoc """
Handles clipboard-related OSC commands.
"""
alias Raxol.Terminal.Clipboard
require Raxol.Core.Runtime.Log
def handle_9(emulator, data) do
case data do
"?" ->
content = Clipboard.get_content(emulator.clipboard)
response = format_clipboard_response(9, content)
{:ok, %{emulator | output_buffer: response}}
content ->
{:ok, new_clipboard} =
Clipboard.set_content(emulator.clipboard, content)
{:ok, %{emulator | clipboard: new_clipboard}}
end
end
def handle_52(emulator, data) do
case parse_52_command(data) do
{:query, :clipboard} ->
content = Clipboard.get_content(emulator.clipboard)
response = format_clipboard_response(52, content)
{:ok, %{emulator | output_buffer: response}}
{:query, :selection} ->
{:ok, content} = Clipboard.get_selection(emulator.clipboard)
response = format_clipboard_response(52, content)
{:ok, %{emulator | output_buffer: response}}
{:set, :clipboard, content} ->
{:ok, new_clipboard} =
Clipboard.set_content(emulator.clipboard, content)
{:ok, %{emulator | clipboard: new_clipboard}}
{:set, :selection, content} ->
{:ok, new_clipboard} =
Clipboard.set_selection(emulator.clipboard, content)
{:ok, %{emulator | clipboard: new_clipboard}}
{:error, _reason} ->
{:error, :invalid_clipboard_command, emulator}
end
end
defp parse_52_command(data) do
case String.split(data, ";", parts: 2) do
["c", "?"] -> {:query, :clipboard}
["s", "?"] -> {:query, :selection}
["c", content] -> {:set, :clipboard, decode_base64(content)}
["s", content] -> {:set, :selection, decode_base64(content)}
_ -> {:error, :invalid_format}
end
end
defp decode_base64(content) do
case Base.decode64(content) do
{:ok, decoded} -> decoded
_ -> content
end
end
defp format_clipboard_response(command, content) do
encoded = Base.encode64(content)
"\e]#{command};c;#{encoded}\e\\"
end
end
# Color sub-module
defmodule Color do
@moduledoc """
Handles color-related OSC commands.
"""
alias Raxol.Terminal.Colors
require Raxol.Core.Runtime.Log
def handle_10(emulator, data) do
case data do
"?" -> handle_color_query(emulator, 10, &Colors.get_foreground/1)
color_spec -> set_color(emulator, color_spec, &Colors.set_foreground/2)
end
end
def handle_11(emulator, data) do
case data do
"?" -> handle_color_query(emulator, 11, &Colors.get_background/1)
color_spec -> set_color(emulator, color_spec, &Colors.set_background/2)
end
end
def handle_17(emulator, data) do
case data do
"?" ->
handle_color_query(emulator, 17, &Colors.get_selection_background/1)
color_spec ->
set_color(emulator, color_spec, &Colors.set_selection_background/2)
end
end
def handle_19(emulator, data) do
case data do
"?" ->
handle_color_query(emulator, 19, &Colors.get_selection_foreground/1)
color_spec ->
set_color(emulator, color_spec, &Colors.set_selection_foreground/2)
end
end
defp handle_color_query(emulator, command, getter) do
color = getter.(emulator.colors)
response = format_color_response(command, color)
{:ok, %{emulator | output_buffer: response}}
end
defp set_color(emulator, color_spec, setter) do
case Raxol.Terminal.Commands.OSCHandler.ColorParser.parse(color_spec) do
{:ok, color} ->
{:ok, new_colors} = setter.(emulator.colors, color)
{:ok, %{emulator | colors: new_colors}}
{:error, _reason} ->
{:error, :invalid_color_spec, emulator}
end
end
defp format_color_response(command, {r, g, b}) do
"\e]#{command};rgb:#{format_hex(r)}/#{format_hex(g)}/#{format_hex(b)}\e\\"
end
defp format_hex(value) do
Integer.to_string(value, 16) |> String.pad_leading(2, "0")
end
end
# ColorParser sub-module
defmodule ColorParser do
@moduledoc """
Parses color specifications from OSC commands.
"""
def parse(color_spec) do
cond do
String.starts_with?(color_spec, "rgb:") ->
parse_rgb(String.trim_leading(color_spec, "rgb:"))
String.starts_with?(color_spec, "#") ->
parse_hex(String.trim_leading(color_spec, "#"))
true ->
parse_name(color_spec)
end
end
defp parse_rgb(rgb_string) do
case String.split(rgb_string, "/") do
[r, g, b] ->
with {:ok, red} <- parse_component(r),
{:ok, green} <- parse_component(g),
{:ok, blue} <- parse_component(b) do
{:ok, {red, green, blue}}
else
_ -> {:error, :invalid_rgb_format}
end
_ ->
{:error, :invalid_rgb_format}
end
end
defp parse_component(hex) do
case Integer.parse(hex, 16) do
{value, ""} when value >= 0 and value <= 255 -> {:ok, value}
_ -> {:error, :invalid_component}
end
end
defp parse_hex(hex_string) do
case String.length(hex_string) do
6 ->
with {:ok, r} <- parse_hex_pair(String.slice(hex_string, 0, 2)),
{:ok, g} <- parse_hex_pair(String.slice(hex_string, 2, 2)),
{:ok, b} <- parse_hex_pair(String.slice(hex_string, 4, 2)) do
{:ok, {r, g, b}}
else
_ -> {:error, :invalid_hex_format}
end
3 ->
with {:ok, r} <- parse_hex_char(String.at(hex_string, 0)),
{:ok, g} <- parse_hex_char(String.at(hex_string, 1)),
{:ok, b} <- parse_hex_char(String.at(hex_string, 2)) do
{:ok, {r * 17, g * 17, b * 17}}
else
_ -> {:error, :invalid_hex_format}
end
_ ->
{:error, :invalid_hex_length}
end
end
defp parse_hex_pair(pair) do
case Integer.parse(pair, 16) do
{value, ""} -> {:ok, value}
_ -> {:error, :invalid_hex}
end
end
defp parse_hex_char(char) do
case Integer.parse(char, 16) do
{value, ""} -> {:ok, value}
_ -> {:error, :invalid_hex}
end
end
defp parse_name(name) do
color_names = %{
"black" => {0, 0, 0},
"red" => {255, 0, 0},
"green" => {0, 255, 0},
"yellow" => {255, 255, 0},
"blue" => {0, 0, 255},
"magenta" => {255, 0, 255},
"cyan" => {0, 255, 255},
"white" => {255, 255, 255}
}
case Map.get(color_names, String.downcase(name)) do
nil -> {:error, :unknown_color_name}
color -> {:ok, color}
end
end
end
# ColorPalette sub-module
defmodule ColorPalette do
@moduledoc """
Handles color palette OSC commands.
"""
require Raxol.Core.Runtime.Log
def handle_4(emulator, data) do
case parse_palette_command(data) do
{:set, index, color} ->
set_palette_color(emulator, index, color)
{:query, index} ->
query_palette_color(emulator, index)
{:reset, index} ->
reset_palette_color(emulator, index)
{:error, _reason} ->
{:error, :invalid_palette_command, emulator}
end
end
defp parse_palette_command(data) do
case String.split(data, ";", parts: 2) do
[index_str, "?"] ->
case Integer.parse(index_str) do
{index, ""} ->
{:query, index}
_ ->
{:error, :invalid_index}
end
[index_str, color_spec] ->
case Integer.parse(index_str) do
{index, ""} ->
if color_spec == "" do
{:reset, index}
else
case Raxol.Terminal.Commands.OSCHandler.ColorParser.parse(color_spec) do
{:ok, color} -> {:set, index, color}
error -> error
end
end
_ ->
{:error, :invalid_index}
end
_ ->
{:error, :invalid_format}
end
end
defp set_palette_color(emulator, index, color)
when index >= 0 and index < 256 do
palette = Map.put(emulator.palette, index, color)
{:ok, %{emulator | palette: palette}}
end
defp set_palette_color(emulator, _index, _color) do
{:error, :index_out_of_range, emulator}
end
defp query_palette_color(emulator, index) when index >= 0 and index < 256 do
color = Map.get(emulator.palette, index, {0, 0, 0})
response = format_palette_response(index, color)
{:ok, %{emulator | output_buffer: response}}
end
defp query_palette_color(emulator, _index) do
{:error, :index_out_of_range, emulator}
end
defp reset_palette_color(emulator, index) when index >= 0 and index < 256 do
default_color = get_default_palette_color(index)
palette = Map.put(emulator.palette, index, default_color)
{:ok, %{emulator | palette: palette}}
end
defp reset_palette_color(emulator, _index) do
{:error, :index_out_of_range, emulator}
end
defp get_default_palette_color(index) do
# Return default ANSI color for the given index
# This is a simplified version - actual defaults depend on terminal
case index do
# Black
0 -> {0, 0, 0}
# Red
1 -> {205, 0, 0}
# Green
2 -> {0, 205, 0}
# Yellow
3 -> {205, 205, 0}
# Blue
4 -> {0, 0, 238}
# Magenta
5 -> {205, 0, 205}
# Cyan
6 -> {0, 205, 205}
# White
7 -> {229, 229, 229}
# Default to black
_ -> {0, 0, 0}
end
end
defp format_palette_response(index, {r, g, b}) do
"\e]4;#{index};rgb:#{format_hex(r)}/#{format_hex(g)}/#{format_hex(b)}\e\\"
end
defp format_hex(value) do
Integer.to_string(value, 16) |> String.pad_leading(4, "0")
end
end
# Window sub-module
defmodule Window do
@moduledoc """
Handles window-related OSC commands.
"""
require Raxol.Core.Runtime.Log
def handle_0(emulator, data) do
# Set icon name and window title
emulator = %{emulator | icon_name: data, window_title: data}
{:ok, emulator}
end
def handle_1(emulator, data) do
# Set icon name
{:ok, %{emulator | icon_name: data}}
end
def handle_2(emulator, data) do
# Set window title
{:ok, %{emulator | window_title: data}}
end
def handle_7(emulator, data) do
# Set current directory (for terminal tabs)
{:ok, %{emulator | current_directory: data}}
end
def handle_8(emulator, data) do
# Set hyperlink
case parse_hyperlink(data) do
{:ok, url, text} ->
hyperlink = %{url: url, text: text}
{:ok, %{emulator | current_hyperlink: hyperlink}}
_ ->
{:error, :invalid_hyperlink, emulator}
end
end
def handle_1337(emulator, data) do
# iTerm2 proprietary escape sequences
handle_iterm2_command(emulator, data)
end
defp parse_hyperlink(data) do
case String.split(data, ";", parts: 2) do
[_params, url] -> {:ok, url, ""}
_ -> {:error, :invalid_format}
end
end
defp handle_iterm2_command(emulator, data) do
case data do
"RemoteHost=" <> host ->
{:ok, %{emulator | remote_host: host}}
"CurrentDir=" <> dir ->
{:ok, %{emulator | current_directory: dir}}
_ ->
# Unsupported iTerm2 command
{:ok, emulator}
end
end
end
# Selection sub-module
defmodule Selection do
@moduledoc """
Handles selection-related OSC commands.
"""
require Raxol.Core.Runtime.Log
def handle_51(emulator, data) do
case data do
"?" ->
# Query selection content
content = Map.get(emulator, :selection_content, "")
response = format_selection_response(content)
{:ok, %{emulator | output_buffer: response}}
content ->
# Set selection content
{:ok, %{emulator | selection_content: content}}
end
end
defp format_selection_response(content) do
encoded = Base.encode64(content)
"\e]51;s;#{encoded}\e\\"
end
end
# FontParser sub-module
defmodule FontParser do
@moduledoc """
Parses font specifications from OSC commands.
"""
def parse(font_spec) do
case parse_font_components(font_spec) do
{:ok, components} -> build_font_map(components)
error -> error
end
end
defp parse_font_components(spec) do
parts = String.split(spec, ":")
case parts do
[family] ->
{:ok, %{family: family}}
[family, size] ->
case Integer.parse(size) do
{size_val, ""} -> {:ok, %{family: family, size: size_val}}
_ -> {:error, :invalid_size}
end
[family, size, style] ->
case Integer.parse(size) do
{size_val, ""} ->
{:ok, %{family: family, size: size_val, style: parse_style(style)}}
_ ->
{:error, :invalid_size}
end
_ ->
{:error, :invalid_format}
end
end
defp parse_style(style) do
style
|> String.downcase()
|> case do
"bold" -> :bold
"italic" -> :italic
"bolditalic" -> :bold_italic
_ -> :regular
end
end
defp build_font_map(components) do
font =
%{
family: "monospace",
size: 12,
style: :regular
}
|> Map.merge(components)
{:ok, font}
end
end
# HyperlinkParser sub-module
defmodule HyperlinkParser do
@moduledoc """
Parses hyperlink specifications from OSC 8 commands.
"""
def parse(data) do
case String.split(data, ";", parts: 2) do
[params, url] ->
parsed_params = parse_params(params)
{:ok, url, parsed_params}
_ ->
{:error, :invalid_format}
end
end
defp parse_params(params_string) do
params_string
|> String.split(":")
|> Enum.map(&parse_param/1)
|> Enum.filter(fn {k, _} -> k != nil end)
|> Map.new()
end
defp parse_param(param) do
case String.split(param, "=", parts: 2) do
[key, value] -> {String.to_atom(key), value}
_ -> {nil, nil}
end
end
end
# SelectionParser sub-module
defmodule SelectionParser do
@moduledoc """
Parses selection specifications from OSC commands.
"""
def parse(data) do
case String.split(data, ";") do
["start", x1, y1, "end", x2, y2] ->
with {x1_val, ""} <- Integer.parse(x1),
{y1_val, ""} <- Integer.parse(y1),
{x2_val, ""} <- Integer.parse(x2),
{y2_val, ""} <- Integer.parse(y2) do
{:ok, %{start: {x1_val, y1_val}, end: {x2_val, y2_val}}}
else
_ -> {:error, :invalid_coordinates}
end
_ ->
{:error, :invalid_format}
end
end
end
end