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
Current section
Files
lib/raxol/terminal/window/manager/window_manager_server.ex
defmodule Raxol.Terminal.Window.Manager.WindowManagerServer do @moduledoc """ GenServer implementation for terminal window management in Raxol. This server provides a pure functional approach to window management, eliminating Process dictionary usage and implementing proper OTP patterns. ## Features - Window creation, destruction, and lifecycle management - Hierarchical window relationships (parent/child) - Window state tracking (active, inactive, minimized, maximized) - Window properties management (title, size, position) - Icon management for windows - Supervised state management with fault tolerance ## State Structure The server maintains state with the following structure: ```elixir %{ windows: %{window_id => Window.t()}, active_window: window_id | nil, window_order: [window_id], # Z-order for stacking window_state: :normal | :minimized | :maximized | :fullscreen, window_size: {width, height}, window_title: String.t(), icon_name: String.t(), icon_title: String.t(), spatial_map: %{}, # For spatial navigation navigation_paths: %{}, # Custom navigation paths next_window_id: integer() } ``` """ use Raxol.Core.Behaviours.BaseManager require Logger alias Raxol.Core.NavigationUtils alias Raxol.Terminal.{Config, Window} @default_state %{ windows: %{}, active_window: nil, window_order: [], window_state: :normal, window_size: {80, 24}, window_title: "", icon_name: "", icon_title: "", spatial_map: %{}, navigation_paths: %{}, next_window_id: 1 } # Client API @doc """ Creates a new window with the given configuration. """ def create_window(config_or_width, height_or_config \\ nil) def create_window(%Config{} = config, nil) do GenServer.call(__MODULE__, {:create_window, config}) end def create_window(width, height) when is_integer(width) and is_integer(height) do create_window(%Config{width: width, height: height}) end def create_window(server, %Config{} = config) when is_atom(server) do GenServer.call(server, {:create_window, config}) end @doc """ Gets a window by ID. """ def get_window(server \\ __MODULE__, window_id) do GenServer.call(server, {:get_window, window_id}) end @doc """ Destroys a window by ID. """ def destroy_window(server \\ __MODULE__, window_id) do GenServer.call(server, {:destroy_window, window_id}) end @doc """ Lists all windows. """ def list_windows(server \\ __MODULE__) do GenServer.call(server, :list_windows) end @doc """ Sets the active window. """ def set_active_window(server \\ __MODULE__, window_id) do GenServer.call(server, {:set_active_window, window_id}) end @doc """ Gets the active window. """ def get_active_window(server \\ __MODULE__) do GenServer.call(server, :get_active_window) end @doc """ Sets the window state (normal, minimized, maximized, fullscreen). """ def set_window_state(state) when state in [:normal, :minimized, :maximized, :fullscreen] do set_window_state(__MODULE__, state) end def set_window_state(server, state) when is_atom(server) and state in [:normal, :minimized, :maximized, :fullscreen] do GenServer.call(server, {:set_window_state, state}) end @doc """ Sets a specific window's state. """ def set_window_state(server, window_id, state) when is_atom(server) and state in [:active, :inactive, :minimized, :maximized] do GenServer.call(server, {:set_window_state_by_id, window_id, state}) end @doc """ Gets the window manager state. """ def get_window_state(server \\ __MODULE__) do GenServer.call(server, :get_window_state) end @doc """ Sets the window size. """ def set_window_size(width, height) when is_integer(width) and width > 0 and is_integer(height) and height > 0 do set_window_size(__MODULE__, width, height) end def set_window_size(server, width, height) when is_atom(server) and is_integer(width) and width > 0 and is_integer(height) and height > 0 do GenServer.call(server, {:set_window_size, width, height}) end @doc """ Sets a specific window's size. """ def set_window_size(server, window_id, width, height) when is_atom(server) do GenServer.call(server, {:set_window_size_by_id, window_id, width, height}) end @doc """ Gets the window size. """ def get_window_size(server \\ __MODULE__) do GenServer.call(server, :get_window_size) end @doc """ Sets the window title. """ def set_window_title(title) when is_binary(title) do set_window_title(__MODULE__, title) end def set_window_title(server, title) when is_atom(server) and is_binary(title) do GenServer.call(server, {:set_window_title, title}) end @doc """ Sets a specific window's title. """ def set_window_title(server, window_id, title) when is_atom(server) do GenServer.call(server, {:set_window_title_by_id, window_id, title}) end @doc """ Sets the icon name. """ def set_icon_name(server \\ __MODULE__, icon_name) when is_binary(icon_name) do GenServer.call(server, {:set_icon_name, icon_name}) end @doc """ Sets the icon title. """ def set_icon_title(server \\ __MODULE__, icon_title) when is_binary(icon_title) do GenServer.call(server, {:set_icon_title, icon_title}) end @doc """ Moves a window in the Z-order. """ def move_window_to_front(server \\ __MODULE__, window_id) do GenServer.call(server, {:move_window_to_front, window_id}) end @doc """ Moves a window to the back in Z-order. """ def move_window_to_back(server \\ __MODULE__, window_id) do GenServer.call(server, {:move_window_to_back, window_id}) end @doc """ Registers a window's spatial position for navigation. """ def register_window_position( server \\ __MODULE__, window_id, x, y, width, height ) do GenServer.call( server, {:register_window_position, window_id, x, y, width, height} ) end @doc """ Defines a navigation path between windows. """ def define_navigation_path(server \\ __MODULE__, from_id, direction, to_id) do GenServer.call(server, {:define_navigation_path, from_id, direction, to_id}) end @doc """ Gets the complete state (for debugging/migration). """ def get_state(server \\ __MODULE__) do GenServer.call(server, :get_state) end @doc """ Resets to initial state. """ def reset(server \\ __MODULE__) do GenServer.call(server, :reset) end @doc """ Split a window horizontally or vertically. """ def split_window(window_id, direction) do split_window(__MODULE__, window_id, direction) end def split_window(server, window_id, direction) when is_atom(server) do GenServer.call(server, {:split_window, window_id, direction}) end @doc """ Updates the window manager configuration. """ def update_config(config) do update_config(__MODULE__, config) end def update_config(server, config) when is_atom(server) do GenServer.call(server, {:update_config, config}) end # BaseManager Callbacks @impl true def init_manager(_opts) do {:ok, @default_state} end @impl true def handle_manager_call({:create_window, config}, _from, state) do window_id = "window_#{state.next_window_id}" window = %Window{ id: window_id, title: Map.get(config, :title, ""), width: config.width, height: config.height, position: {Map.get(config, :x, 0), Map.get(config, :y, 0)}, size: {config.width, config.height}, state: :inactive } new_windows = Map.put(state.windows, window_id, window) new_window_order = [window_id | state.window_order] new_state = %{ state | windows: new_windows, window_order: new_window_order, next_window_id: state.next_window_id + 1 } # Activate if it's the first window new_state = maybe_activate_first_window(new_state, state.active_window, window_id) {:reply, {:ok, window}, new_state} end @impl true def handle_manager_call({:get_window, window_id}, _from, state) do case Map.get(state.windows, window_id) do nil -> {:reply, {:error, :not_found}, state} window -> {:reply, {:ok, window}, state} end end @impl true def handle_manager_call({:destroy_window, window_id}, _from, state) do case Map.get(state.windows, window_id) do nil -> {:reply, {:error, :not_found}, state} _window -> new_windows = Map.delete(state.windows, window_id) new_window_order = List.delete(state.window_order, window_id) # Update active window if necessary new_active = update_active_after_destroy( state.active_window, window_id, new_window_order ) # Clean up spatial map and navigation paths new_spatial_map = Map.delete(state.spatial_map, window_id) new_nav_paths = Map.delete(state.navigation_paths, window_id) new_state = %{ state | windows: new_windows, window_order: new_window_order, active_window: new_active, spatial_map: new_spatial_map, navigation_paths: new_nav_paths } {:reply, :ok, new_state} end end @impl true def handle_manager_call(:list_windows, _from, state) do windows = Map.values(state.windows) {:reply, {:ok, windows}, state} end @impl true def handle_manager_call({:set_active_window, window_id}, _from, state) do handle_set_active_window( Map.has_key?(state.windows, window_id), window_id, state ) end @impl true def handle_manager_call(:get_active_window, _from, state) do case state.active_window do nil -> {:reply, nil, state} id -> case Map.get(state.windows, id) do nil -> {:reply, nil, state} window -> {:reply, {:ok, window}, state} end end end @impl true def handle_manager_call({:set_window_state, state_value}, _from, state) do new_state = %{state | window_state: state_value} {:reply, :ok, new_state} end @impl true def handle_manager_call( {:set_window_state_by_id, window_id, state_value}, _from, state ) do case Map.get(state.windows, window_id) do nil -> {:reply, {:error, :not_found}, state} window -> updated_window = %{window | state: state_value} new_windows = Map.put(state.windows, window_id, updated_window) new_state = %{state | windows: new_windows} {:reply, {:ok, updated_window}, new_state} end end @impl true def handle_manager_call(:get_window_state, _from, state) do {:reply, state.window_state, state} end @impl true def handle_manager_call({:set_window_size, width, height}, _from, state) do new_state = %{state | window_size: {width, height}} {:reply, :ok, new_state} end @impl true def handle_manager_call( {:set_window_size_by_id, window_id, width, height}, _from, state ) do case Map.get(state.windows, window_id) do nil -> {:reply, {:error, :not_found}, state} window -> updated_window = %{ window | width: width, height: height, size: {width, height} } new_windows = Map.put(state.windows, window_id, updated_window) new_state = %{state | windows: new_windows} {:reply, {:ok, updated_window}, new_state} end end @impl true def handle_manager_call(:get_window_size, _from, state) do {:reply, state.window_size, state} end @impl true def handle_manager_call({:set_window_title, title}, _from, state) do new_state = %{state | window_title: title} {:reply, :ok, new_state} end @impl true def handle_manager_call( {:set_window_title_by_id, window_id, title}, _from, state ) do case Map.get(state.windows, window_id) do nil -> {:reply, {:error, :not_found}, state} window -> updated_window = %{window | title: title} new_windows = Map.put(state.windows, window_id, updated_window) new_state = %{state | windows: new_windows} {:reply, {:ok, updated_window}, new_state} end end @impl true def handle_manager_call({:set_icon_name, icon_name}, _from, state) do new_state = %{state | icon_name: icon_name} {:reply, :ok, new_state} end @impl true def handle_manager_call({:set_icon_title, icon_title}, _from, state) do new_state = %{state | icon_title: icon_title} {:reply, :ok, new_state} end @impl true def handle_manager_call({:move_window_to_front, window_id}, _from, state) do handle_move_window_to_front( Map.has_key?(state.windows, window_id), window_id, state ) end @impl true def handle_manager_call({:move_window_to_back, window_id}, _from, state) do handle_move_window_to_back( Map.has_key?(state.windows, window_id), window_id, state ) end @impl true def handle_manager_call({:set_window_position, window_id, x, y}, _from, state) do case Map.get(state.windows, window_id) do nil -> {:reply, {:error, :not_found}, state} window -> updated_window = %{window | position: {x, y}} new_windows = Map.put(state.windows, window_id, updated_window) new_state = %{state | windows: new_windows} {:reply, {:ok, updated_window}, new_state} end end @impl true def handle_manager_call( {:create_child_window, parent_id, config}, _from, state ) do case Map.get(state.windows, parent_id) do nil -> {:reply, {:error, :parent_not_found}, state} parent -> child_id = "window_#{state.next_window_id}" child = %Window{ id: child_id, title: Map.get(config, :title, ""), width: config.width, height: config.height, position: {Map.get(config, :x, 0), Map.get(config, :y, 0)}, size: {config.width, config.height}, state: :inactive, parent: parent_id } # Update parent to include child updated_parent = %{parent | children: [child_id | parent.children]} new_windows = state.windows |> Map.put(child_id, child) |> Map.put(parent_id, updated_parent) new_state = %{ state | windows: new_windows, window_order: [child_id | state.window_order], next_window_id: state.next_window_id + 1 } {:reply, {:ok, child}, new_state} end end @impl true def handle_manager_call({:get_child_windows, parent_id}, _from, state) do case Map.get(state.windows, parent_id) do nil -> {:reply, {:error, :not_found}, state} parent -> children = Enum.map(parent.children, &Map.get(state.windows, &1)) |> Enum.filter(&(&1 != nil)) {:reply, {:ok, children}, state} end end @impl true def handle_manager_call({:get_parent_window, child_id}, _from, state) do case Map.get(state.windows, child_id) do nil -> {:reply, {:error, :not_found}, state} child -> case child.parent do nil -> {:reply, {:error, :no_parent}, state} parent_id -> case Map.get(state.windows, parent_id) do nil -> {:reply, {:error, :parent_not_found}, state} parent -> {:reply, {:ok, parent}, state} end end end end @impl true def handle_manager_call( {:register_window_position, window_id, x, y, width, height}, _from, state ) do position_data = %{ id: window_id, x: x, y: y, width: width, height: height, center_x: x + div(width, 2), center_y: y + div(height, 2) } new_spatial_map = Map.put(state.spatial_map, window_id, position_data) new_state = %{state | spatial_map: new_spatial_map} {:reply, :ok, new_state} end @impl true def handle_manager_call( {:define_navigation_path, from_id, direction, to_id}, _from, state ) do new_state = NavigationUtils.define_navigation_path(state, from_id, direction, to_id) {:reply, :ok, new_state} end @impl true def handle_manager_call(:get_state, _from, state) do # Return a map compatible with the original implementation legacy_state = %{ title: state.window_title, icon_name: state.icon_name, icon_title: state.icon_title, windows: state.windows, active_window: state.active_window, state: state.window_state, size: state.window_size } {:reply, legacy_state, 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} parent_window -> child_window_id = "window_#{state.next_window_id}" child_window = %Window{ id: child_window_id, title: "", size: calculate_split_size(parent_window.size, direction), position: {0, 0}, parent: window_id } updated_parent = %{ parent_window | children: [child_window_id | parent_window.children] } new_state = %{ state | windows: Map.merge(state.windows, %{ window_id => updated_parent, child_window_id => child_window }), window_order: [child_window_id | state.window_order], next_window_id: state.next_window_id + 1 } {:reply, {:ok, child_window_id}, new_state} end end @impl true def handle_manager_call(:reset, _from, _state) do {:reply, :ok, @default_state} end @impl true def handle_manager_call({:update_config, config}, _from, state) do new_state = Map.merge(state, config) {:reply, :ok, new_state} end # Private helper functions defp maybe_activate_first_window(new_state, nil, window_id) do %{new_state | active_window: window_id} end defp maybe_activate_first_window(new_state, _active_window, _window_id) do new_state end defp update_active_after_destroy(active_window, window_id, new_window_order) when active_window == window_id do List.first(new_window_order) end defp update_active_after_destroy(active_window, _window_id, _new_window_order) do active_window end defp calculate_split_size({width, height}, :horizontal) do {width, div(height, 2)} end defp calculate_split_size({width, height}, :vertical) do {div(width, 2), height} end defp handle_set_active_window(false, _window_id, state) do {:reply, {:error, :not_found}, state} end defp handle_set_active_window(true, window_id, state) do # Update window states new_windows = state.windows |> Enum.map(fn {id, window} -> new_state = determine_window_state(id == window_id, window.state) {id, %{window | state: new_state}} end) |> Enum.into(%{}) new_state = %{state | windows: new_windows, active_window: window_id} {:reply, :ok, new_state} end defp determine_window_state(true, _current_state) do :active end defp determine_window_state(false, :active) do :inactive end defp determine_window_state(false, current_state) do current_state end defp handle_move_window_to_front(false, _window_id, state) do {:reply, {:error, :not_found}, state} end defp handle_move_window_to_front(true, window_id, state) do new_order = [window_id | List.delete(state.window_order, window_id)] new_state = %{state | window_order: new_order} {:reply, :ok, new_state} end defp handle_move_window_to_back(false, _window_id, state) do {:reply, {:error, :not_found}, state} end defp handle_move_window_to_back(true, window_id, state) do new_order = List.delete(state.window_order, window_id) ++ [window_id] new_state = %{state | window_order: new_order} {:reply, :ok, new_state} end # Missing API function implementations @doc """ Sets window position. """ def set_window_position(window_id, x, y) do GenServer.call(__MODULE__, {:set_window_position, window_id, x, y}) end @doc """ Creates a child window. """ def create_child_window(parent_id, config) do GenServer.call(__MODULE__, {:create_child_window, parent_id, config}) end @doc """ Gets child windows. """ def get_child_windows(parent_id) do GenServer.call(__MODULE__, {:get_child_windows, parent_id}) end @doc """ Gets parent window. """ def get_parent_window(child_id) do GenServer.call(__MODULE__, {:get_parent_window, child_id}) endend