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