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 terminal commands unified_command_handler.ex
Raw

lib/raxol/terminal/commands/unified_command_handler.ex

defmodule Raxol.Terminal.Commands.UnifiedCommandHandler do
@moduledoc """
Unified command handler that consolidates all terminal command processing.
This module replaces the fragmented handler pattern with a single, organized
command processing system that handles:
- Cursor movement and positioning commands
- Device status and attribute commands
- Erase operations (screen, line, character)
- CSI (Control Sequence Introducer) commands
- OSC (Operating System Command) sequences
- DCS (Device Control String) commands
- Mode setting and resetting
- Text formatting and styling
- Window operations
- Buffer operations
## Design Principles
1. **Unified Interface**: Single entry point for all command processing
2. **Command Routing**: Intelligent routing based on command type and parameters
3. **State Management**: Consistent state handling across all command types
4. **Error Handling**: Centralized error handling with graceful fallbacks
5. **Performance**: Optimized dispatching with minimal overhead
6. **Extensibility**: Easy to add new command types and handlers
## Command Categories
- **Cursor Commands**: CUP, CUU, CUD, CUF, CUB, HVP, etc.
- **Erase Commands**: ED, EL, ECH, etc.
- **Device Commands**: DA, DSR, CPR, etc.
- **Mode Commands**: SM, RM, DECSET, DECRST, etc.
- **Text Commands**: SGR, TBC, CTC, etc.
- **Window Commands**: Window resize, title setting, etc.
- **Buffer Commands**: Buffer switching, scrolling, etc.
"""
alias Raxol.Terminal.Emulator
alias Raxol.Terminal.{OutputManager, ScreenBuffer}
alias Raxol.Terminal.Cell
alias Raxol.Terminal.ANSI.TextFormatting
alias Raxol.Terminal.Cursor.Manager, as: CursorManager
alias Raxol.Terminal.Commands.CursorUtils
alias Raxol.Terminal.Buffer.Eraser
require Raxol.Core.Runtime.Log
@type command_result :: {:ok, Emulator.t()} | {:error, atom(), Emulator.t()}
@type command_type :: :csi | :osc | :dcs | :escape | :control
@type command_params :: %{
type: command_type(),
command: String.t(),
params: list(integer()),
intermediates: String.t(),
private_markers: String.t()
}
## Public API
@doc """
Processes any terminal command with unified handling.
This is the main entry point for all command processing.
"""
@spec handle_command(Emulator.t(), command_params()) :: command_result()
def handle_command(emulator, %{type: type, command: command} = cmd_params) do
Raxol.Core.Runtime.Log.debug("Processing #{type} command: #{command}")
case route_command(type, command, cmd_params) do
{:ok, handler_func} ->
execute_command(emulator, handler_func, cmd_params)
{:error, :unknown_command} ->
Raxol.Core.Runtime.Log.warning("Unknown #{type} command: #{command}")
# Graceful fallback
{:ok, emulator}
end
end
@doc """
Handles CSI (Control Sequence Introducer) commands.
"""
@spec handle_csi(Emulator.t(), String.t(), list(integer()), String.t()) ::
command_result()
def handle_csi(emulator, command, params \\ [], intermediates \\ "") do
cmd_params = %{
type: :csi,
command: command,
params: params,
intermediates: intermediates,
private_markers: ""
}
handle_command(emulator, cmd_params)
end
@doc """
Handles OSC (Operating System Command) sequences.
"""
@spec handle_osc(Emulator.t(), String.t(), String.t()) :: command_result()
def handle_osc(emulator, command, data) do
cmd_params = %{
type: :osc,
command: command,
params: [data],
intermediates: "",
private_markers: ""
}
handle_command(emulator, cmd_params)
end
## Command Routing
defp route_command(:csi, command, _params) do
case categorize_csi_command(command) do
{:cursor, handler} -> {:ok, handler}
{:erase, handler} -> {:ok, handler}
{:device, handler} -> {:ok, handler}
{:mode, handler} -> {:ok, handler}
{:text, handler} -> {:ok, handler}
{:scroll, handler} -> {:ok, handler}
{:buffer, handler} -> {:ok, handler}
{:tab, handler} -> {:ok, handler}
:unknown -> {:error, :unknown_command}
end
end
defp route_command(:osc, command, _params) do
case command do
"0" -> {:ok, &handle_window_title/3}
"1" -> {:ok, &handle_window_icon/3}
"2" -> {:ok, &handle_window_title/3}
"4" -> {:ok, &handle_color_palette/3}
"10" -> {:ok, &handle_foreground_color/3}
"11" -> {:ok, &handle_background_color/3}
_ -> {:error, :unknown_command}
end
end
defp route_command(:dcs, command, _params) do
case command do
"q" -> {:ok, &handle_sixel/3}
_ -> {:error, :unknown_command}
end
end
defp route_command(_type, _command, _params) do
{:error, :unknown_command}
end
## CSI Command Categorization
defp categorize_csi_command(command) do
cond do
command in ~w[A B C D E F G H f d] -> {:cursor, get_cursor_handler(command)}
command in ~w[J K X] -> {:erase, get_erase_handler(command)}
command in ~w[c n] -> {:device, get_device_handler(command)}
command in ~w[h l] -> {:mode, get_mode_handler(command)}
command == "m" -> {:text, &handle_sgr/3}
command in ~w[S T] -> {:scroll, get_scroll_handler(command)}
command in ~w[L M P @] -> {:buffer, get_buffer_handler(command)}
command == "g" -> {:tab, &handle_tab_clear/3}
true -> :unknown
end
end
defp get_cursor_handler(command) do
cursor_handlers()[command]
end
defp get_erase_handler(command) do
erase_handlers()[command]
end
defp get_device_handler(command) do
device_handlers()[command]
end
defp get_mode_handler(command) do
mode_handlers()[command]
end
defp get_scroll_handler(command) do
scroll_handlers()[command]
end
defp get_buffer_handler(command) do
buffer_handlers()[command]
end
# Handler maps
defp cursor_handlers do
%{
"A" => &handle_cursor_up/3,
"B" => &handle_cursor_down/3,
"C" => &handle_cursor_forward/3,
"D" => &handle_cursor_backward/3,
"E" => &handle_cursor_next_line/3,
"F" => &handle_cursor_previous_line/3,
"G" => &handle_cursor_horizontal_absolute/3,
"H" => &handle_cursor_position/3,
"f" => &handle_cursor_position/3, # HVP - same as CUP
"d" => &handle_cursor_vertical_absolute/3 # VPA
}
end
defp erase_handlers do
%{
"J" => &handle_erase_display/3,
"K" => &handle_erase_line/3,
"X" => &handle_erase_character/3
}
end
defp device_handlers do
%{
"c" => &handle_device_attributes/3,
"n" => &handle_device_status_report/3
}
end
defp mode_handlers do
%{
"h" => &handle_set_mode/3,
"l" => &handle_reset_mode/3
}
end
defp scroll_handlers do
%{
"S" => &handle_scroll_up/3,
"T" => &handle_scroll_down/3
}
end
defp buffer_handlers do
%{
"L" => &handle_insert_lines/3,
"M" => &handle_delete_lines/3,
"P" => &handle_delete_characters/3,
"@" => &handle_insert_characters/3
}
end
## Command Execution
defp execute_command(emulator, handler_func, cmd_params) do
try do
handler_func.(emulator, cmd_params, %{})
rescue
error ->
Raxol.Core.Runtime.Log.error(
"Command execution failed: #{inspect(error)}"
)
{:error, :command_execution_failed, emulator}
catch
:throw, reason ->
Raxol.Core.Runtime.Log.error("Command threw: #{inspect(reason)}")
{:error, :command_thrown, emulator}
end
end
## Cursor Movement Commands
defp handle_cursor_up(emulator, %{params: params}, _context) do
amount = get_param(params, 0, 1)
move_cursor(emulator, :up, amount)
end
defp handle_cursor_down(emulator, %{params: params}, _context) do
amount = get_param(params, 0, 1)
move_cursor(emulator, :down, amount)
end
defp handle_cursor_forward(emulator, %{params: params}, _context) do
amount = get_param(params, 0, 1)
move_cursor(emulator, :right, amount)
end
defp handle_cursor_backward(emulator, %{params: params}, _context) do
amount = get_param(params, 0, 1)
move_cursor(emulator, :left, amount)
end
defp handle_cursor_next_line(emulator, %{params: params}, _context) do
amount = get_param(params, 0, 1)
with {:ok, emulator} <- move_cursor(emulator, :down, amount) do
move_cursor_to_column(emulator, 0)
end
end
defp handle_cursor_previous_line(emulator, %{params: params}, _context) do
amount = get_param(params, 0, 1)
with {:ok, emulator} <- move_cursor(emulator, :up, amount) do
move_cursor_to_column(emulator, 0)
end
end
defp handle_cursor_horizontal_absolute(emulator, %{params: params}, _context) do
# Convert to 0-based
col = get_param(params, 0, 1) - 1
move_cursor_to_column(emulator, col)
end
defp handle_cursor_position(emulator, %{params: params}, _context) do
# Convert to 0-based
row = get_param(params, 0, 1) - 1
# Convert to 0-based
col = get_param(params, 1, 1) - 1
set_cursor_position(emulator, {row, col})
end
defp handle_cursor_vertical_absolute(emulator, %{params: params}, _context) do
# Convert to 0-based
row = get_param(params, 0, 1) - 1
{_, col} = get_cursor_position(emulator)
set_cursor_position(emulator, {row, col})
end
## Erase Commands
defp handle_erase_display(emulator, %{params: params}, _context) do
mode = get_param(params, 0, 0)
perform_screen_erase(emulator, mode)
end
defp handle_erase_line(emulator, %{params: params}, _context) do
mode = get_param(params, 0, 0)
perform_line_erase(emulator, mode)
end
defp handle_erase_character(emulator, %{params: params}, _context) do
count = get_param(params, 0, 1)
perform_character_erase(emulator, count)
end
## Device Commands
defp handle_device_attributes(
emulator,
%{params: params, intermediates: intermediates},
_context
) do
case {params, intermediates} do
{[], ""} ->
# Primary DA with no params and no intermediates
response = generate_primary_da_response()
{:ok, OutputManager.write(emulator, response)}
{[0], ""} ->
# Primary DA with explicit 0 param
response = generate_primary_da_response()
{:ok, OutputManager.write(emulator, response)}
{[], ">"} ->
# Secondary DA with no params
response = generate_secondary_da_response()
{:ok, OutputManager.write(emulator, response)}
{[0], ">"} ->
# Secondary DA with explicit 0 param
response = generate_secondary_da_response()
{:ok, OutputManager.write(emulator, response)}
_ ->
# Ignore other DA requests
{:ok, emulator}
end
end
defp handle_device_status_report(emulator, %{params: params}, _context) do
code = get_param(params, 0, 5)
Raxol.Core.Runtime.Log.debug("DSR request: code=#{code}")
response =
case code do
# Device OK
5 -> "\e[0n"
# Cursor Position Report
6 -> generate_cursor_position_report(emulator)
_ -> nil
end
Raxol.Core.Runtime.Log.debug("DSR response: #{inspect(response)}")
case response do
nil -> {:ok, emulator}
_ -> {:ok, OutputManager.write(emulator, response)}
end
end
## Mode Commands
defp handle_set_mode(
emulator,
%{params: params, private_markers: private},
_context
) do
case private do
"?" -> handle_dec_private_mode(emulator, params, :set)
_ -> handle_ansi_mode(emulator, params, :set)
end
end
defp handle_reset_mode(
emulator,
%{params: params, private_markers: private},
_context
) do
case private do
"?" -> handle_dec_private_mode(emulator, params, :reset)
_ -> handle_ansi_mode(emulator, params, :reset)
end
end
## Text Formatting Commands
defp handle_sgr(emulator, %{params: params}, _context) do
apply_text_formatting(emulator, params)
end
## Scrolling Commands
defp handle_scroll_up(emulator, %{params: params}, _context) do
lines = get_param(params, 0, 1)
perform_scroll(emulator, :up, lines)
end
defp handle_scroll_down(emulator, %{params: params}, _context) do
lines = get_param(params, 0, 1)
perform_scroll(emulator, :down, lines)
end
## OSC Commands
defp handle_window_title(emulator, %{params: [title]}, _context) do
# Store window title in emulator state
updated_emulator = %{emulator | window_title: title}
{:ok, updated_emulator}
end
defp handle_window_icon(emulator, %{params: [icon_name]}, _context) do
# Store icon name in emulator state
updated_emulator = %{emulator | icon_name: icon_name}
{:ok, updated_emulator}
end
defp handle_color_palette(emulator, %{params: [color_spec]}, _context) do
# Parse and apply color palette changes
apply_color_palette_change(emulator, color_spec)
end
defp handle_foreground_color(emulator, %{params: [color_spec]}, _context) do
apply_foreground_color(emulator, color_spec)
end
defp handle_background_color(emulator, %{params: [color_spec]}, _context) do
apply_background_color(emulator, color_spec)
end
## Tab Commands
defp handle_tab_clear(emulator, %{params: params}, _context) do
mode = get_param(params, 0, 0)
clear_tabs(emulator, mode)
end
## Buffer Operations
defp handle_insert_lines(emulator, %{params: params}, _context) do
count = get_param(params, 0, 1)
perform_insert_lines(emulator, count)
end
defp handle_delete_lines(emulator, %{params: params}, _context) do
count = get_param(params, 0, 1)
perform_delete_lines(emulator, count)
end
defp handle_delete_characters(emulator, %{params: params}, _context) do
count = get_param(params, 0, 1)
perform_delete_characters(emulator, count)
end
defp handle_insert_characters(emulator, %{params: params}, _context) do
count = get_param(params, 0, 1)
perform_insert_characters(emulator, count)
end
## DCS Commands
defp handle_sixel(emulator, %{params: [sixel_data]}, _context) do
process_sixel_graphics(emulator, sixel_data)
end
## Helper Functions
defp get_param(params, index, default) do
case Enum.at(params, index) do
nil -> default
0 -> default
value when is_integer(value) and value > 0 -> value
_ -> default
end
end
defp move_cursor(emulator, direction, amount) do
cursor_pos = get_cursor_position(emulator)
{row, col} = cursor_pos
{new_row, new_col} =
CursorUtils.calculate_new_cursor_position(
{row, col},
direction,
amount,
emulator.width,
emulator.height
)
set_cursor_position(emulator, {new_row, new_col})
end
defp move_cursor_to_column(emulator, col) do
{row, _} = get_cursor_position(emulator)
clamped_col = max(0, min(col, emulator.width - 1))
set_cursor_position(emulator, {row, clamped_col})
end
defp get_cursor_position(emulator) do
extract_position_from_cursor(emulator.cursor)
end
defp extract_position_from_cursor(cursor) do
case cursor do
pid when is_pid(pid) -> get_position_from_pid(pid)
%{position: pos} when is_tuple(pos) -> pos
%{row: row, col: col} -> {row, col}
_ -> {0, 0}
end
end
defp get_position_from_pid(pid) do
case CursorManager.get_position(pid) do
{:ok, pos} when is_tuple(pos) -> pos
pos when is_tuple(pos) -> pos
_ -> {0, 0}
end
end
defp set_cursor_position(emulator, {row, col}) do
clamped_row = max(0, min(row, emulator.height - 1))
clamped_col = max(0, min(col, emulator.width - 1))
case emulator.cursor do
pid when is_pid(pid) ->
:ok = CursorManager.set_position(pid, {clamped_row, clamped_col})
{:ok, emulator}
cursor_struct ->
updated_cursor = %{cursor_struct | position: {clamped_row, clamped_col}}
{:ok, %{emulator | cursor: updated_cursor}}
end
end
defp perform_screen_erase(emulator, mode) do
{active_buffer, cursor_pos, default_style} = get_buffer_state(emulator)
Raxol.Core.Runtime.Log.debug(
"perform_screen_erase: mode=#{mode}, cursor_pos=#{inspect(cursor_pos)}"
)
# Ensure buffer has the correct cursor position
active_buffer = %{active_buffer | cursor_position: cursor_pos}
new_buffer =
case mode do
0 ->
{row, col} = cursor_pos
Eraser.clear_screen_from(active_buffer, row, col, default_style)
1 ->
{row, col} = cursor_pos
Eraser.clear_screen_to(active_buffer, row, col, default_style)
2 ->
Eraser.clear_screen(active_buffer, default_style)
3 ->
Eraser.clear_scrollback(active_buffer)
_ ->
active_buffer
end
# Update the emulator with the new buffer
updated_emulator = update_emulator_buffer(emulator, new_buffer)
{:ok, updated_emulator}
end
defp perform_line_erase(emulator, mode) do
{active_buffer, cursor_pos, default_style} = get_buffer_state(emulator)
# Ensure buffer has the correct cursor position
active_buffer = %{active_buffer | cursor_position: cursor_pos}
new_buffer =
case mode do
0 ->
{row, col} = cursor_pos
Eraser.clear_line_from(active_buffer, row, col, default_style)
1 ->
{row, col} = cursor_pos
Eraser.clear_line_to(active_buffer, row, col, default_style)
2 ->
{row, _col} = cursor_pos
Eraser.clear_line(active_buffer, row, default_style)
_ ->
active_buffer
end
# Update the emulator with the new buffer
updated_emulator = update_emulator_buffer(emulator, new_buffer)
{:ok, updated_emulator}
end
defp perform_character_erase(emulator, count) do
{active_buffer, cursor_pos, _default_style} = get_buffer_state(emulator)
# Ensure buffer has the correct cursor position
active_buffer = %{active_buffer | cursor_position: cursor_pos}
{row, col} = cursor_pos
new_buffer = Eraser.erase_chars(active_buffer, row, col, count)
# Update the emulator with the new buffer
updated_emulator = update_emulator_buffer(emulator, new_buffer)
{:ok, updated_emulator}
end
defp get_buffer_state(emulator) do
active_buffer = Emulator.get_screen_buffer(emulator)
cursor_pos = get_cursor_position(emulator)
default_style = TextFormatting.new()
{active_buffer, cursor_pos, default_style}
end
defp update_emulator_buffer(emulator, new_buffer) do
# Update the appropriate buffer based on which is active
case Map.get(emulator, :active_buffer_type, :main) do
:alternate ->
%{emulator | alternate_screen_buffer: new_buffer}
_ ->
%{emulator | main_screen_buffer: new_buffer}
end
end
defp generate_primary_da_response do
# VT102 compatible terminal
"\e[?6c"
end
defp generate_secondary_da_response do
# Secondary DA response with version info (basic xterm-like response)
"\e[>0;0;0c"
end
defp generate_cursor_position_report(emulator) do
pos = get_cursor_position(emulator)
{row, col} = case pos do
{r, c} when is_integer(r) and is_integer(c) -> {r, c}
_ -> {0, 0}
end
# Convert to 1-based
"\e[#{row + 1};#{col + 1}R"
end
defp handle_ansi_mode(emulator, params, action) do
# Handle standard ANSI modes
updated_emulator =
Enum.reduce(params, emulator, fn mode, acc ->
apply_ansi_mode(acc, mode, action)
end)
{:ok, updated_emulator}
end
defp handle_dec_private_mode(emulator, params, action) do
# Handle DEC private modes (prefixed with ?)
updated_emulator =
Enum.reduce(params, emulator, fn mode, acc ->
apply_dec_private_mode(acc, mode, action)
end)
{:ok, updated_emulator}
end
defp apply_ansi_mode(emulator, mode, action) do
# Apply standard ANSI mode settings
case {mode, action} do
{4, :set} -> %{emulator | insert_mode: true}
{4, :reset} -> %{emulator | insert_mode: false}
{20, :set} -> %{emulator | automatic_newline: true}
{20, :reset} -> %{emulator | automatic_newline: false}
_ -> emulator
end
end
defp apply_dec_private_mode(emulator, mode, action) do
# Apply DEC private mode settings
case {mode, action} do
{1, :set} ->
%{emulator | cursor_keys_mode: :application}
{1, :reset} ->
%{emulator | cursor_keys_mode: :normal}
{25, :set} ->
%{emulator | cursor_visible: true}
{25, :reset} ->
%{emulator | cursor_visible: false}
{47, :set} ->
switch_to_alternate_screen(emulator)
{47, :reset} ->
switch_to_main_screen(emulator)
# Mode 1049: Save cursor & switch to alternate screen buffer
{1049, :set} ->
emulator
|> save_cursor_position()
|> switch_to_alternate_screen_with_clear()
{1049, :reset} ->
emulator
|> switch_to_main_screen()
|> restore_cursor_position()
_ ->
emulator
end
end
defp apply_text_formatting(emulator, params) do
# Apply SGR (Select Graphic Rendition) parameters
updated_emulator =
Enum.reduce(params, emulator, fn param, acc ->
apply_sgr_parameter(acc, param)
end)
{:ok, updated_emulator}
end
defp apply_sgr_parameter(emulator, param) do
current_style = get_current_text_style(emulator)
new_style = apply_sgr_formatting(current_style, param)
set_current_text_style(emulator, new_style)
end
defp apply_sgr_formatting(style, param) do
case categorize_sgr_parameter(param) do
{:reset, _} -> TextFormatting.reset_attributes(style)
{:set_attribute, attribute} -> apply_text_attribute(style, attribute)
{:reset_attribute, attribute} -> reset_text_attribute(style, attribute)
{:color, type, value} -> apply_color(style, type, value)
:unknown -> style
end
end
defp categorize_sgr_parameter(param) do
sgr_mappings()[param] || categorize_sgr_range(param)
end
defp categorize_sgr_range(param) do
cond do
param >= 30 and param <= 37 -> {:color, :foreground, param - 30}
param >= 40 and param <= 47 -> {:color, :background, param - 40}
true -> :unknown
end
end
defp sgr_mappings do
%{
0 => {:reset, :all},
1 => {:set_attribute, :bold},
2 => {:set_attribute, :faint},
3 => {:set_attribute, :italic},
4 => {:set_attribute, :underline},
5 => {:set_attribute, :blink},
7 => {:set_attribute, :reverse},
8 => {:set_attribute, :conceal},
9 => {:set_attribute, :strikethrough},
22 => {:reset_attribute, :bold_faint},
23 => {:reset_attribute, :italic},
24 => {:reset_attribute, :underline},
25 => {:reset_attribute, :blink},
27 => {:reset_attribute, :reverse},
28 => {:reset_attribute, :conceal},
29 => {:reset_attribute, :strikethrough},
39 => {:color, :foreground, :reset},
49 => {:color, :background, :reset}
}
end
defp apply_text_attribute(style, attribute) do
case attribute do
:bold -> TextFormatting.set_bold(style)
:faint -> TextFormatting.set_faint(style)
:italic -> TextFormatting.set_italic(style)
:underline -> TextFormatting.set_underline(style)
:blink -> TextFormatting.set_blink(style)
:reverse -> TextFormatting.set_reverse(style)
:conceal -> TextFormatting.set_conceal(style)
:strikethrough -> TextFormatting.set_strikethrough(style)
end
end
defp reset_text_attribute(style, attribute) do
case attribute do
:bold_faint -> TextFormatting.reset_bold(style) |> TextFormatting.reset_faint()
:italic -> TextFormatting.reset_italic(style)
:underline -> TextFormatting.reset_underline(style)
:blink -> TextFormatting.reset_blink(style)
:reverse -> TextFormatting.reset_reverse(style)
:conceal -> TextFormatting.reset_conceal(style)
:strikethrough -> TextFormatting.reset_strikethrough(style)
end
end
defp apply_color(style, type, value) do
case {type, value} do
{:foreground, :reset} -> TextFormatting.reset_foreground(style)
{:background, :reset} -> TextFormatting.reset_background(style)
{:foreground, color_value} -> TextFormatting.set_foreground(style, color_value)
{:background, color_value} -> TextFormatting.set_background(style, color_value)
end
end
defp perform_scroll(emulator, direction, lines) do
active_buffer = Emulator.get_screen_buffer(emulator)
_new_buffer =
case direction do
:up -> ScreenBuffer.scroll_up(active_buffer, lines)
:down -> ScreenBuffer.scroll_down(active_buffer, lines)
end
# Note: Buffer operations are performed in-place
{:ok, emulator}
end
defp clear_tabs(emulator, mode) do
case mode do
0 -> clear_tab_at_cursor(emulator)
3 -> clear_all_tabs(emulator)
_ -> {:ok, emulator}
end
end
# Buffer operation implementations
defp perform_insert_lines(emulator, count) do
active_buffer = Emulator.get_screen_buffer(emulator)
{y, _} = get_cursor_position(emulator)
style = get_default_style(active_buffer)
# Check for scroll region
case Map.get(emulator, :scroll_region) do
{scroll_top, scroll_bottom} when y >= scroll_top and y <= scroll_bottom ->
updated_buffer =
insert_lines_within_scroll_region(
active_buffer,
y,
count,
style,
scroll_top,
scroll_bottom
)
# CRITICAL FIX: Actually update the emulator with new buffer
updated_emulator = update_emulator_buffer(emulator, updated_buffer)
{:ok, updated_emulator}
_ ->
updated_buffer = insert_lines_normal(active_buffer, y, count, style)
# CRITICAL FIX: Actually update the emulator with new buffer
updated_emulator = update_emulator_buffer(emulator, updated_buffer)
{:ok, updated_emulator}
end
end
defp perform_delete_lines(emulator, count) do
active_buffer = Emulator.get_screen_buffer(emulator)
{y, _} = get_cursor_position(emulator)
style = get_default_style(active_buffer)
# Check for scroll region
case Map.get(emulator, :scroll_region) do
{scroll_top, scroll_bottom} when y >= scroll_top and y <= scroll_bottom ->
Raxol.Core.Runtime.Log.debug("DL using scroll region: y=#{y}, region=#{scroll_top}..#{scroll_bottom}")
updated_buffer =
delete_lines_within_scroll_region(
active_buffer,
y,
count,
style,
scroll_top,
scroll_bottom
)
# CRITICAL FIX: Actually update the emulator with new buffer
updated_emulator = update_emulator_buffer(emulator, updated_buffer)
{:ok, updated_emulator}
{scroll_top, scroll_bottom} ->
# Scroll region exists but cursor is outside it - no effect
Raxol.Core.Runtime.Log.debug("DL outside scroll region: y=#{y}, scroll_region={#{scroll_top}, #{scroll_bottom}} - no effect")
{:ok, emulator}
nil ->
# No scroll region - normal delete
Raxol.Core.Runtime.Log.debug("DL with no scroll region: y=#{y} - normal delete")
updated_buffer = delete_lines_normal(active_buffer, y, count, style)
updated_emulator = update_emulator_buffer(emulator, updated_buffer)
{:ok, updated_emulator}
end
end
defp perform_delete_characters(emulator, count) do
active_buffer = Emulator.get_screen_buffer(emulator)
{y, x} = get_cursor_position(emulator)
style = get_default_style(active_buffer)
updated_buffer = delete_characters(active_buffer, y, x, count, style)
# CRITICAL FIX: Actually update the emulator with new buffer
updated_emulator = update_emulator_buffer(emulator, updated_buffer)
{:ok, updated_emulator}
end
defp perform_insert_characters(emulator, count) do
active_buffer = Emulator.get_screen_buffer(emulator)
{y, x} = get_cursor_position(emulator)
Raxol.Core.Runtime.Log.debug("perform_insert_characters: count=#{count}, pos=({#{x}, #{y}})")
Raxol.Core.Runtime.Log.debug("Buffer type: #{inspect(active_buffer.__struct__)}")
Raxol.Core.Runtime.Log.debug("Buffer cells nil?: #{inspect(is_nil(active_buffer.cells))}")
Raxol.Core.Runtime.Log.debug("Buffer cells length: #{inspect(length(active_buffer.cells || []))}")
Raxol.Core.Runtime.Log.debug("Buffer dimensions: #{active_buffer.width}x#{active_buffer.height}")
Raxol.Core.Runtime.Log.debug("First cell value: #{inspect(Enum.at(active_buffer.cells, 0))}")
style = get_default_style(active_buffer)
Raxol.Core.Runtime.Log.debug("Style: #{inspect(style)}")
updated_buffer = insert_characters(active_buffer, y, x, count, style)
Raxol.Core.Runtime.Log.debug("Updated buffer cells nil?: #{inspect(is_nil(updated_buffer.cells))}")
if not is_nil(active_buffer.cells) and not is_nil(updated_buffer.cells) do
line_before = Enum.at(active_buffer.cells, y)
line_after = Enum.at(updated_buffer.cells, y)
Raxol.Core.Runtime.Log.debug("y=#{y}, line_before: #{inspect(line_before |> Enum.take(3))}")
Raxol.Core.Runtime.Log.debug("y=#{y}, line_after: #{inspect(line_after |> Enum.take(3))}")
end
# Update the emulator with the new buffer
updated_emulator = update_emulator_buffer(emulator, updated_buffer)
{:ok, updated_emulator}
end
defp get_default_style(buffer) do
case buffer do
%{default_style: style} when not is_nil(style) ->
style
_ ->
TextFormatting.new()
end
end
defp insert_lines_normal(buffer, y, count, style) do
# Create blank lines
blank_cell = Cell.new(" ", style)
blank_line = List.duplicate(blank_cell, buffer.width)
blank_lines = List.duplicate(blank_line, count)
# Insert lines at position y
{lines_before, lines_after} = Enum.split(buffer.cells, y)
# Keep only enough lines after to maintain total buffer size
lines_to_keep = max(0, buffer.height - y - count)
kept_lines = Enum.take(lines_after, lines_to_keep)
final_cells = lines_before ++ blank_lines ++ kept_lines
%{buffer | cells: final_cells}
end
defp insert_lines_within_scroll_region(
buffer,
y,
count,
style,
scroll_top,
scroll_bottom
) do
# Create blank lines using Cell.new
blank_cell = Cell.new(" ", style)
blank_line = List.duplicate(blank_cell, buffer.width)
blank_lines_to_insert = List.duplicate(blank_line, count)
# Split buffer into regions
{lines_before_scroll, rest} = Enum.split(buffer.cells, scroll_top)
{scroll_region_lines, lines_after_scroll} =
Enum.split(rest, scroll_bottom - scroll_top + 1)
# Split scroll region at insertion point
insertion_point = y - scroll_top
{scroll_before, scroll_after} =
Enum.split(scroll_region_lines, insertion_point)
# When inserting lines, we keep lines before insertion point,
# add blank lines, and keep as many lines after as will fit
# Lines that don't fit are discarded (scrolled off bottom of region)
max_lines_in_region = scroll_bottom - scroll_top + 1
available_space_after = max_lines_in_region - insertion_point - count
kept_lines =
if available_space_after > 0 do
# Keep only lines that fit in the remaining space
Enum.take(scroll_after, available_space_after)
else
# No space left, all lines after insertion are pushed out
[]
end
# Reconstruct scroll region
new_scroll_region = scroll_before ++ blank_lines_to_insert ++ kept_lines
# Pad to correct size
padded_scroll_region =
if length(new_scroll_region) < max_lines_in_region do
new_scroll_region ++
List.duplicate(
blank_line,
max_lines_in_region - length(new_scroll_region)
)
else
Enum.take(new_scroll_region, max_lines_in_region)
end
# Combine all parts
final_cells =
lines_before_scroll ++ padded_scroll_region ++ lines_after_scroll
%{buffer | cells: final_cells}
end
defp delete_lines_within_scroll_region(
buffer,
y,
count,
style,
scroll_top,
scroll_bottom
) do
# Create blank lines for replacement
blank_cell = Cell.new(" ", style)
blank_line = List.duplicate(blank_cell, buffer.width)
blank_lines_to_add = List.duplicate(blank_line, count)
# Split buffer into regions
{lines_before_scroll, rest} = Enum.split(buffer.cells, scroll_top)
{scroll_region_lines, lines_after_scroll} =
Enum.split(rest, scroll_bottom - scroll_top + 1)
# Split scroll region at deletion point
deletion_point = y - scroll_top
{scroll_before, scroll_after} =
Enum.split(scroll_region_lines, deletion_point)
# Remove deleted lines and keep remaining lines
remaining_lines = Enum.drop(scroll_after, count)
# Reconstruct scroll region with blank lines at bottom
max_lines_in_region = scroll_bottom - scroll_top + 1
new_scroll_region = scroll_before ++ remaining_lines ++ blank_lines_to_add
# Ensure correct size
padded_scroll_region =
if length(new_scroll_region) < max_lines_in_region do
new_scroll_region ++
List.duplicate(
blank_line,
max_lines_in_region - length(new_scroll_region)
)
else
Enum.take(new_scroll_region, max_lines_in_region)
end
# Combine all parts
final_cells =
lines_before_scroll ++ padded_scroll_region ++ lines_after_scroll
%{buffer | cells: final_cells}
end
defp delete_lines_normal(buffer, y, count, style) do
blank_cell = Cell.new(" ", style)
blank_line = List.duplicate(blank_cell, buffer.width)
# Remove lines starting at y
{lines_before, lines_after} = Enum.split(buffer.cells, y)
remaining_lines = Enum.drop(lines_after, count)
# Add blank lines at the end to maintain buffer size
blank_lines_needed = min(count, buffer.height - y)
blank_lines = List.duplicate(blank_line, blank_lines_needed)
final_cells = lines_before ++ remaining_lines ++ blank_lines
%{buffer | cells: final_cells}
end
defp delete_characters(buffer, y, x, count, style) do
# Get the line at y
case Enum.at(buffer.cells, y) do
nil ->
buffer
line ->
# Split line at x
{chars_before, chars_after} = Enum.split(line, x)
remaining_chars = Enum.drop(chars_after, count)
# Pad with blanks
blank_cell = Cell.new(" ", style)
blanks_needed = count
blank_chars = List.duplicate(blank_cell, blanks_needed)
new_line = chars_before ++ remaining_chars ++ blank_chars
# Ensure line doesn't exceed width
final_line = Enum.take(new_line, buffer.width)
# Replace line in buffer
updated_cells = List.replace_at(buffer.cells, y, final_line)
%{buffer | cells: updated_cells}
end
end
defp insert_characters(buffer, y, x, count, style) do
# Get the line at y
case Enum.at(buffer.cells, y) do
nil ->
buffer
line ->
# Split line at x
{chars_before, chars_after} = Enum.split(line, x)
# Insert blank characters
blank_cell = Cell.new(" ", style)
blank_chars = List.duplicate(blank_cell, count)
# Combine and ensure we don't exceed width
new_line = chars_before ++ blank_chars ++ chars_after
final_line = Enum.take(new_line, buffer.width)
# Replace line in buffer
updated_cells = List.replace_at(buffer.cells, y, final_line)
%{buffer | cells: updated_cells}
end
end
# Placeholder implementations for various helper functions
defp get_current_text_style(emulator),
do: emulator.current_style || TextFormatting.new()
defp set_current_text_style(emulator, style),
do: %{emulator | current_style: style}
defp switch_to_alternate_screen(emulator) do
# Switch to alternate screen buffer, preserving main buffer
%{emulator | screen_mode: :alternate, active_buffer_type: :alternate}
end
defp switch_to_alternate_screen_with_clear(emulator) do
# Switch to alternate screen and clear it
emulator
|> switch_to_alternate_screen()
|> clear_alternate_buffer()
end
defp switch_to_main_screen(emulator) do
%{emulator | screen_mode: :main, active_buffer_type: :main}
end
defp save_cursor_position(emulator) do
cursor = emulator.cursor
updated_cursor = %{
cursor
| saved_row: cursor.row,
saved_col: cursor.col,
saved_position: {cursor.row, cursor.col}
}
%{emulator | cursor: updated_cursor}
end
defp restore_cursor_position(emulator) do
cursor = emulator.cursor
{new_row, new_col} =
case {cursor.saved_row, cursor.saved_col} do
{nil, nil} -> {0, 0}
{row, col} -> {row, col}
end
updated_cursor = %{
cursor
| row: new_row,
col: new_col,
position: {new_row, new_col}
}
%{emulator | cursor: updated_cursor}
end
defp clear_alternate_buffer(emulator) do
# Clear the alternate buffer by creating a new blank one
blank_buffer = ScreenBuffer.new(emulator.width, emulator.height)
%{emulator | alternate_screen_buffer: blank_buffer}
end
defp clear_tab_at_cursor(emulator), do: {:ok, emulator}
defp clear_all_tabs(emulator), do: {:ok, emulator}
defp apply_color_palette_change(emulator, _color_spec), do: {:ok, emulator}
defp apply_foreground_color(emulator, _color_spec), do: {:ok, emulator}
defp apply_background_color(emulator, _color_spec), do: {:ok, emulator}
defp process_sixel_graphics(emulator, _sixel_data), do: {:ok, emulator}
end