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.Terminal.{Config, Window} alias Raxol.Terminal.Window.Manager.{NavigationOps, StateOps} @compile {:no_warn_undefined, [ Raxol.Terminal.Window.Manager.StateOps, Raxol.Terminal.Window.Manager.NavigationOps ]} @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 @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}) end # BaseManager Callbacks @impl Raxol.Core.Behaviours.BaseManager def init_manager(_opts) do {:ok, @default_state} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:create_window, config}, _from, state) do window_id = "window_#{state.next_window_id}" window = StateOps.build_window(config, window_id) 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 } new_state = StateOps.maybe_activate_first_window( new_state, state.active_window, window_id ) {:reply, {:ok, window}, new_state} end @impl Raxol.Core.Behaviours.BaseManager 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 Raxol.Core.Behaviours.BaseManager 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) new_active = StateOps.update_active_after_destroy( state.active_window, window_id, new_window_order ) new_state = %{ state | windows: new_windows, window_order: new_window_order, active_window: new_active, spatial_map: Map.delete(state.spatial_map, window_id), navigation_paths: Map.delete(state.navigation_paths, window_id) } {:reply, :ok, new_state} end end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call(:list_windows, _from, state) do {:reply, {:ok, Map.values(state.windows)}, state} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_active_window, window_id}, _from, state) do StateOps.apply_set_active_window( Map.has_key?(state.windows, window_id), window_id, state ) end @impl Raxol.Core.Behaviours.BaseManager 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 Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_window_state, state_value}, _from, state) do {:reply, :ok, %{state | window_state: state_value}} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call( {:set_window_state_by_id, window_id, state_value}, _from, state ) do case StateOps.update_window_by_id( state, window_id, &%{&1 | state: state_value} ) do {:ok, updated_window, new_state} -> {:reply, {:ok, updated_window}, new_state} {:error, :not_found} -> {:reply, {:error, :not_found}, state} end end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call(:get_window_state, _from, state) do {:reply, state.window_state, state} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_window_size, width, height}, _from, state) do {:reply, :ok, %{state | window_size: {width, height}}} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call( {:set_window_size_by_id, window_id, width, height}, _from, state ) do update_fn = &%{&1 | width: width, height: height, size: {width, height}} case StateOps.update_window_by_id(state, window_id, update_fn) do {:ok, updated_window, new_state} -> {:reply, {:ok, updated_window}, new_state} {:error, :not_found} -> {:reply, {:error, :not_found}, state} end end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call(:get_window_size, _from, state) do {:reply, state.window_size, state} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_window_title, title}, _from, state) do {:reply, :ok, %{state | window_title: title}} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call( {:set_window_title_by_id, window_id, title}, _from, state ) do case StateOps.update_window_by_id(state, window_id, &%{&1 | title: title}) do {:ok, updated_window, new_state} -> {:reply, {:ok, updated_window}, new_state} {:error, :not_found} -> {:reply, {:error, :not_found}, state} end end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_icon_name, icon_name}, _from, state) do {:reply, :ok, %{state | icon_name: icon_name}} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_icon_title, icon_title}, _from, state) do {:reply, :ok, %{state | icon_title: icon_title}} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:move_window_to_front, window_id}, _from, state) do StateOps.apply_move_to_front( Map.has_key?(state.windows, window_id), window_id, state ) end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:move_window_to_back, window_id}, _from, state) do StateOps.apply_move_to_back( Map.has_key?(state.windows, window_id), window_id, state ) end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:set_window_position, window_id, x, y}, _from, state) do case StateOps.update_window_by_id( state, window_id, &%{&1 | position: {x, y}} ) do {:ok, updated_window, new_state} -> {:reply, {:ok, updated_window}, new_state} {:error, :not_found} -> {:reply, {:error, :not_found}, state} end end @impl Raxol.Core.Behaviours.BaseManager 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 = StateOps.build_child_window(config, child_id, parent_id) 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 Raxol.Core.Behaviours.BaseManager 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 = parent.children |> Enum.map(&Map.get(state.windows, &1)) |> Enum.reject(&is_nil/1) {:reply, {:ok, children}, state} end end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:get_parent_window, child_id}, _from, state) do with {:ok, child} <- fetch_window(state, child_id), {:ok, parent_id} <- fetch_parent_id(child), {:ok, parent} <- fetch_window(state, parent_id, :parent_not_found) do {:reply, {:ok, parent}, state} else {:error, reason} -> {:reply, {:error, reason}, state} end end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call( {:register_window_position, window_id, x, y, width, height}, _from, state ) do new_state = NavigationOps.register_window_position( state, window_id, x, y, width, height ) {:reply, :ok, new_state} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call( {:define_navigation_path, from_id, direction, to_id}, _from, state ) do new_state = NavigationOps.define_navigation_path(state, from_id, direction, to_id) {:reply, :ok, new_state} end @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call(:get_state, _from, state) do {:reply, StateOps.build_legacy_state(state), state} end @impl Raxol.Core.Behaviours.BaseManager 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: StateOps.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 Raxol.Core.Behaviours.BaseManager def handle_manager_call(:reset, _from, _state) do {:reply, :ok, @default_state} end @configurable_keys ~w(window_size window_title icon_name icon_title)a @impl Raxol.Core.Behaviours.BaseManager def handle_manager_call({:update_config, config}, _from, state) do filtered = Map.take(config, @configurable_keys) {:reply, :ok, Map.merge(state, filtered)} end # Private helpers defp fetch_window(state, window_id, error \\ :not_found) do case Map.get(state.windows, window_id) do nil -> {:error, error} window -> {:ok, window} end end defp fetch_parent_id(%{parent: nil}), do: {:error, :no_parent} defp fetch_parent_id(%{parent: parent_id}), do: {:ok, parent_id} end