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/window/window_server.ex
defmodule Raxol.Terminal.Window.WindowServer 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 Raxol.Core.Behaviours.BaseManager
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 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
@impl true
def init_manager(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
@impl true
def handle_manager_call({:create_window, opts}, _from, state) do
{window_id, new_state} = do_create_window(opts, state)
{:reply, {:ok, window_id}, new_state}
end
@impl true
def handle_manager_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
@impl true
def handle_manager_call(:get_active_window, _from, state) do
{:reply, {:ok, state.active_window}, state}
end
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_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
@impl true
def handle_manager_call({:update_config, config}, _from, state) do
new_state = %{state | config: Map.merge(state.config, config)}
{:reply, :ok, new_state}
end
@impl true
def handle_manager_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