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(),
iconified: boolean(),
previous_size: {non_neg_integer(), non_neg_integer()} | nil,
stacking_order: :normal | :above | :below,
parent_id: window_id() | nil,
children: [window_id()],
split_type: :horizontal | :vertical | :none,
buffer_id: String.t() | nil,
renderer_id: String.t() | nil
}
@type t :: window_state()
@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
# Handle both keyword lists and maps
opts_map =
case is_map(opts) do
true -> opts
false -> Map.new(opts || [])
end
config = %{
default_size: Map.get(opts_map, :default_size, {80, 24}),
max_size: Map.get(opts_map, :max_size, {200, 50}),
default_buffer_id: Map.get(opts_map, :default_buffer_id),
default_renderer_id: Map.get(opts_map, :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 ->
handle_split_window_request(window, direction, state)
end
end
def handle_call(:get_active_window, _from, state) do
{:reply, {:ok, state.active_window}, state}
end
def handle_call({:get_window_state, window_id}, _from, state) do
case Map.get(state.windows, window_id) do
nil -> {:reply, {:error, :window_not_found}, state}
window -> {:reply, {:ok, window}, state}
end
end
def handle_call({:set_title, window_id, title}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
updated_window = %{window | title: title}
new_state = %{
state
| windows: Map.put(state.windows, window_id, updated_window)
}
{:reply, :ok, new_state}
end
end
def handle_call({:set_icon_name, window_id, icon_name}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
updated_window = %{window | icon_name: icon_name}
new_state = %{
state
| windows: Map.put(state.windows, window_id, updated_window)
}
{:reply, :ok, new_state}
end
end
def handle_call({:resize, window_id, width, height}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
updated_window = %{window | size: {width, height}}
new_state = %{
state
| windows: Map.put(state.windows, window_id, updated_window)
}
{:reply, :ok, new_state}
end
end
def handle_call({:move, window_id, x, y}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
updated_window = %{window | position: {x, y}}
new_state = %{
state
| windows: Map.put(state.windows, window_id, updated_window)
}
{:reply, :ok, new_state}
end
end
def handle_call({:set_stacking_order, window_id, order}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
updated_window = %{window | stacking_order: order}
new_state = %{
state
| windows: Map.put(state.windows, window_id, updated_window)
}
{:reply, :ok, new_state}
end
end
def handle_call({:set_maximized, window_id, maximized}, _from, state) do
case Map.get(state.windows, window_id) do
nil ->
{:reply, {:error, :window_not_found}, state}
window ->
{new_size, new_previous_size} =
case maximized do
true -> {state.config.max_size, window.size}
false -> {window.previous_size || state.config.default_size, nil}
end
updated_window = %{
window
| maximized: maximized,
size: new_size,
previous_size: new_previous_size
}
new_state = %{
state
| windows: Map.put(state.windows, window_id, updated_window)
}
{:reply, :ok, new_state}
end
end
def handle_call({:set_active_window, window_id}, _from, state) do
case Map.get(state.windows, window_id) do
nil -> {:reply, {:error, :window_not_found}, state}
_window -> {:reply, :ok, %{state | active_window: window_id}}
end
end
def handle_call({:close_window, window_id}, _from, state) do
case do_close_window(window_id, state) do
{:ok, new_state} -> {:reply, :ok, new_state}
{:error, new_state} -> {:reply, {:error, :window_not_found}, new_state}
end
end
def handle_call({:update_config, config}, _from, state) do
new_state = %{state | config: Map.merge(state.config, config)}
{:reply, :ok, new_state}
end
def handle_call(:cleanup, _from, state) do
# Clean up all windows
new_state = %{state | windows: %{}, active_window: nil}
{:reply, :ok, new_state}
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) when is_list(opts) 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),
iconified: Keyword.get(opts, :iconified, false),
previous_size: nil,
stacking_order: :normal,
parent_id: Keyword.get(opts, :parent_id),
children: [],
split_type: :none,
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_create_window(opts, state) when is_map(opts) do
window_id = state.next_id
window = %{
id: window_id,
title: Map.get(opts, :title, ""),
icon_name: Map.get(opts, :icon_name),
size: Map.get(opts, :size, state.config.default_size),
position: Map.get(opts, :position, {0, 0}),
maximized: Map.get(opts, :maximized, false),
iconified: Map.get(opts, :iconified, false),
previous_size: nil,
stacking_order: :normal,
parent_id: Map.get(opts, :parent_id),
children: [],
split_type: :none,
buffer_id: Map.get(opts, :buffer_id, state.config.default_buffer_id),
renderer_id: Map.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 ->
new_state = close_child_windows(window, state)
new_state = update_parent_window(window, new_state)
final_state = remove_window_and_update_active(window_id, new_state)
{:ok, final_state}
end
end
defp close_child_windows(window, state) do
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)
end
defp update_parent_window(window, state) do
case window.parent_id do
nil ->
state
parent_id ->
case Map.get(state.windows, parent_id) do
nil -> state
parent -> update_parent_children(parent, window.id, state)
end
end
end
defp update_parent_children(parent, window_id, state) do
updated_parent = %{
parent
| children: List.delete(parent.children, window_id)
}
%{state | windows: Map.put(state.windows, parent.id, updated_parent)}
end
defp remove_window_and_update_active(window_id, state) do
state = %{state | windows: Map.delete(state.windows, window_id)}
case state.active_window == window_id do
true ->
next_window_id = Map.keys(state.windows) |> List.first()
%{state | active_window: next_window_id}
false ->
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
defp handle_split_window_request(window, direction, state) do
case direction do
direction when direction in [:horizontal, :vertical] ->
{:ok, new_window_id, new_state} =
do_split_window(window, direction, state)
{:reply, {:ok, new_window_id}, new_state}
_ ->
{:reply, {:error, :invalid_direction}, state}
end
end
end