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 window unified_window.ex
Raw

lib/raxol/terminal/window/unified_window.ex

defmodule Raxol.Terminal.Window.UnifiedWindow do
@moduledoc """
A unified window manager for terminal applications.
This module provides a GenServer-based window management system that handles
window creation, splitting, resizing, and other window operations.
"""
use GenServer
require Logger
@type window_id :: non_neg_integer()
@type window_state :: %{
id: window_id(),
title: String.t() | nil,
icon_name: String.t() | nil,
size: {non_neg_integer(), non_neg_integer()},
position: {non_neg_integer(), non_neg_integer()},
maximized: boolean(),
previous_size: {non_neg_integer(), non_neg_integer()} | nil,
stacking_order: non_neg_integer(),
parent_id: window_id() | nil,
children: [window_id()],
split_type: :horizontal | :vertical | nil,
buffer_id: String.t() | nil,
renderer_id: String.t() | nil
}
@type state :: %{
windows: %{window_id() => window_state()},
active_window: window_id() | nil,
next_id: non_neg_integer(),
config: %{
default_size: {non_neg_integer(), non_neg_integer()},
max_size: {non_neg_integer(), non_neg_integer()},
default_buffer_id: String.t() | nil,
default_renderer_id: String.t() | nil
}
}
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def create_window(opts \\ []) do
GenServer.call(__MODULE__, {:create_window, opts})
end
def split_window(window_id, direction) do
GenServer.call(__MODULE__, {:split_window, window_id, direction})
end
def close_window(window_id) do
GenServer.call(__MODULE__, {:close_window, window_id})
end
def set_title(window_id, title) do
GenServer.call(__MODULE__, {:set_title, window_id, title})
end
def set_icon_name(window_id, name) do
GenServer.call(__MODULE__, {:set_icon_name, window_id, name})
end
def resize(window_id, width, height) do
GenServer.call(__MODULE__, {:resize, window_id, width, height})
end
def move(window_id, x, y) do
GenServer.call(__MODULE__, {:move, window_id, x, y})
end
def set_stacking_order(window_id, order) do
GenServer.call(__MODULE__, {:set_stacking_order, window_id, order})
end
def set_maximized(window_id, maximized) do
GenServer.call(__MODULE__, {:set_maximized, window_id, maximized})
end
def set_active_window(window_id) do
GenServer.call(__MODULE__, {:set_active_window, window_id})
end
def get_window_state(window_id) do
GenServer.call(__MODULE__, {:get_window_state, window_id})
end
def get_active_window do
GenServer.call(__MODULE__, :get_active_window)
end
def update_config(config) do
GenServer.call(__MODULE__, {:update_config, config})
end
def cleanup do
GenServer.call(__MODULE__, :cleanup)
end
# Server callbacks
def init(opts) do
config = %{
default_size: Keyword.get(opts, :default_size, {80, 24}),
max_size: Keyword.get(opts, :max_size, {200, 100}),
default_buffer_id: Keyword.get(opts, :default_buffer_id),
default_renderer_id: Keyword.get(opts, :default_renderer_id)
}
{:ok,
%{
windows: %{},
active_window: nil,
next_id: 1,
config: config
}}
end
def handle_call({:create_window, opts}, _from, state) do
{window_id, new_state} = do_create_window(opts, state)
{:reply, {:ok, window_id}, new_state}
end
def handle_call({:split_window, window_id, direction}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
case direction do
direction when direction in [:horizontal, :vertical] ->
case do_split_window(window, direction, state) do
{:ok, new_window_id, new_state} ->
{:reply, {:ok, new_window_id}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
_ ->
{:reply, {:error, :invalid_direction}, state}
end
end
end
defp calculate_split_sizes({width, height}, :horizontal) do
half = div(width, 2)
{{half, height}, {half, height}}
end
defp calculate_split_sizes({width, height}, :vertical) do
half = div(height, 2)
{{width, half}, {width, half}}
end
# Private helper functions
defp do_create_window(opts, state) do
window_id = state.next_id
window = %{
id: window_id,
title: Keyword.get(opts, :title),
icon_name: Keyword.get(opts, :icon_name),
size: Keyword.get(opts, :size, state.config.default_size),
position: Keyword.get(opts, :position, {0, 0}),
maximized: Keyword.get(opts, :maximized, false),
previous_size: nil,
stacking_order: window_id,
parent_id: Keyword.get(opts, :parent_id),
children: [],
split_type: nil,
buffer_id: Keyword.get(opts, :buffer_id, state.config.default_buffer_id),
renderer_id:
Keyword.get(opts, :renderer_id, state.config.default_renderer_id)
}
new_state = %{
state
| windows: Map.put(state.windows, window_id, window),
next_id: state.next_id + 1,
active_window: window_id
}
{window_id, new_state}
end
defp do_close_window(window_id, state) do
case Map.get(state.windows, window_id) do
nil ->
{:error, state}
window ->
# Close all child windows recursively
new_state =
Enum.reduce(window.children, state, fn child_id, acc ->
case do_close_window(child_id, acc) do
{:ok, acc2} -> acc2
{:error, acc2} -> acc2
end
end)
# Remove window from parent's children list
new_state =
if window.parent_id do
case Map.get(new_state.windows, window.parent_id) do
nil ->
new_state
parent ->
updated_parent = %{
parent
| children: List.delete(parent.children, window_id)
}
%{
new_state
| windows:
Map.put(
new_state.windows,
window.parent_id,
updated_parent
)
}
end
else
new_state
end
# Remove the window
final_state = %{
new_state
| windows: Map.delete(new_state.windows, window_id)
}
# Update active window if needed
final_state =
if final_state.active_window == window_id do
# Find the next available window
next_window_id = Map.keys(final_state.windows) |> List.first()
%{final_state | active_window: next_window_id}
else
final_state
end
{:ok, final_state}
end
end
defp do_split_window(window, direction, state) do
{parent_size, child_size} = calculate_split_sizes(window.size, direction)
{new_window_id, state1} =
do_create_window(
%{
size: child_size,
position: window.position,
buffer_id: Map.get(state.config, :default_buffer_id),
renderer_id: Map.get(state.config, :default_renderer_id)
},
state
)
updated_window = %{
window
| size: parent_size,
split_type: direction,
children: [new_window_id | window.children]
}
new_window = Map.get(state1.windows, new_window_id)
updated_new_window = %{new_window | parent_id: window.id}
new_state = %{
state1
| windows:
state1.windows
|> Map.put(window.id, updated_window)
|> Map.put(new_window_id, updated_new_window)
}
{:ok, new_window_id, new_state}
end
end