defmodule Raxol.Terminal.Tab.Manager do @moduledoc """ Manages terminal tabs and their associated sessions. This module handles: - Creation, deletion, and switching of terminal tabs - Tab state and configuration management - Tab stop management for terminal operations """ use Raxol.Core.Behaviours.BaseManager require Logger # Types @type tab_id :: String.t() @type tab_state :: :active | :inactive | :hidden @type tab_config :: %{ title: String.t(), working_directory: String.t(), command: String.t() | nil, state: tab_state, window_id: String.t() | nil } @type t :: %__MODULE__{ tabs: %{tab_id() => tab_config()}, active_tab: tab_id() | nil, next_tab_id: non_neg_integer(), tab_stops: MapSet.t(), default_tab_width: pos_integer() } defstruct tabs: %{}, active_tab: nil, next_tab_id: 1, tab_stops: MapSet.new(), default_tab_width: 8 # Client API # BaseManager provides start_link @doc """ Creates a new tab manager instance. """ @spec new() :: t() def new do %__MODULE__{} end @doc """ Creates a new tab with the given configuration. ## Parameters * `manager` - The tab manager instance * `config` - The tab configuration (optional) ## Returns `{:ok, tab_id, updated_manager}` on success `{:error, reason}` on failure """ @spec create_tab(t(), map()) :: {:ok, tab_id(), t()} def create_tab(manager, config \\ %{}) do tab_id = generate_tab_id(manager) default_config = %{ title: "Tab #{tab_id}", working_directory: File.cwd!(), command: nil, state: :inactive, window_id: nil } config = Map.merge(default_config, config || %{}) updated_manager = %{ manager | tabs: Map.put(manager.tabs, tab_id, config), next_tab_id: manager.next_tab_id + 1 } {:ok, tab_id, updated_manager} end @doc """ Deletes a tab by its ID. ## Parameters * `manager` - The tab manager instance * `tab_id` - The ID of the tab to delete ## Returns `{:ok, updated_manager}` on success `{:error, :tab_not_found}` if the tab doesn't exist """ @spec delete_tab(t(), tab_id()) :: {:ok, t()} | {:error, :tab_not_found} def delete_tab(manager, tab_id) do case Map.get(manager.tabs, tab_id) do nil -> {:error, :tab_not_found} __tab -> updated_manager = %{ manager | tabs: Map.delete(manager.tabs, tab_id), active_tab: case manager.active_tab == tab_id do true -> nil false -> manager.active_tab end } {:ok, updated_manager} end end @doc """ Switches to a different tab. ## Parameters * `manager` - The tab manager instance * `tab_id` - The ID of the tab to switch to ## Returns `{:ok, updated_manager}` on success `{:error, :tab_not_found}` if the tab doesn't exist """ @spec switch_tab(t(), tab_id()) :: {:ok, t()} | {:error, :tab_not_found} def switch_tab(manager, tab_id) do case Map.get(manager.tabs, tab_id) do nil -> {:error, :tab_not_found} tab -> updated_manager = %{ manager | active_tab: tab_id, tabs: Map.put(manager.tabs, tab_id, %{tab | state: :active}) } {:ok, updated_manager} end end @doc """ Gets the configuration for a specific tab. ## Parameters * `manager` - The tab manager instance * `tab_id` - The ID of the tab ## Returns `{:ok, config}` on success `{:error, :tab_not_found}` if the tab doesn't exist """ @spec get_tab_config(t(), tab_id()) :: {:ok, tab_config()} | {:error, :tab_not_found} def get_tab_config(manager, tab_id) do case Map.get(manager.tabs, tab_id) do nil -> {:error, :tab_not_found} config -> {:ok, config} end end @doc """ Updates the configuration for a specific tab. ## Parameters * `manager` - The tab manager instance * `tab_id` - The ID of the tab * `updates` - The configuration updates to apply ## Returns `{:ok, updated_manager}` on success `{:error, :tab_not_found}` if the tab doesn't exist """ @spec update_tab_config(t(), tab_id(), map()) :: {:ok, t()} | {:error, :tab_not_found} def update_tab_config(manager, tab_id, updates) do case Map.get(manager.tabs, tab_id) do nil -> {:error, :tab_not_found} current_config -> updated_config = Map.merge(current_config, updates) updated_manager = %{ manager | tabs: Map.put(manager.tabs, tab_id, updated_config) } {:ok, updated_manager} end end @doc """ Lists all tabs. ## Parameters * `manager` - The tab manager instance ## Returns A map of tab IDs to tab configurations """ @spec list_tabs(t()) :: %{tab_id() => tab_config()} def list_tabs(manager) do manager.tabs end @doc """ Gets the active tab ID. ## Parameters * `manager` - The tab manager instance ## Returns The active tab ID or nil if no tab is active """ @spec get_active_tab(t()) :: tab_id() | nil def get_active_tab(manager) do manager.active_tab end # Tab Stop Management Functions @doc """ Sets a horizontal tab stop at the current cursor position. """ @spec set_horizontal_tab(t()) :: t() def set_horizontal_tab(manager) do # Set tab stop at current position (assume position 0 if not provided) # In a real implementation, this would receive the current cursor position current_position = 0 %{manager | tab_stops: MapSet.put(manager.tab_stops, current_position)} end @doc """ Sets a horizontal tab stop at the specified position. """ @spec set_horizontal_tab(t(), non_neg_integer()) :: t() def set_horizontal_tab(manager, position) when is_integer(position) and position >= 0 do %{manager | tab_stops: MapSet.put(manager.tab_stops, position)} end @doc """ Clears a tab stop at the specified position. """ @spec clear_tab_stop(t(), pos_integer()) :: t() def clear_tab_stop(manager, position) when is_integer(position) and position >= 0 do %{manager | tab_stops: MapSet.delete(manager.tab_stops, position)} end @doc """ Clears all tab stops. """ @spec clear_all_tab_stops(t()) :: t() def clear_all_tab_stops(manager) do %{manager | tab_stops: MapSet.new()} end @doc """ Gets the next tab stop position from the current position. """ @spec get_next_tab_stop(t()) :: pos_integer() def get_next_tab_stop(manager) do # Calculate next tab stop based on current position (assume position 0 if not provided) # In a real implementation, this would receive the current cursor position current_position = 0 find_next_tab_stop(current_position, manager.tab_stops) end @doc """ Gets the next tab stop position from a specific current position. """ @spec get_next_tab_stop(t(), non_neg_integer()) :: pos_integer() def get_next_tab_stop(manager, current_position) when is_integer(current_position) and current_position >= 0 do find_next_tab_stop(current_position, manager.tab_stops) end # Server Callbacks @impl true def init_manager(opts) do state = %__MODULE__{ default_tab_width: Keyword.get(opts, :default_tab_width, 8) } {:ok, state} end @impl true def handle_manager_call({:create_tab, config}, _from, state) do tab_id = generate_tab_id(state) default_config = %{ title: "Tab #{tab_id}", working_directory: File.cwd!(), command: nil, state: :inactive, window_id: nil } config = Map.merge(default_config, config) updated_state = %{ state | tabs: Map.put(state.tabs, tab_id, config), next_tab_id: state.next_tab_id + 1 } {:reply, {:ok, tab_id}, updated_state} end def handle_manager_call({:delete_tab, tab_id}, _from, state) do case Map.has_key?(state.tabs, tab_id) do true -> updated_state = %{ state | tabs: Map.delete(state.tabs, tab_id), active_tab: case state.active_tab == tab_id do true -> nil false -> state.active_tab end } {:reply, {:ok, updated_state}, updated_state} false -> {:reply, {:error, :tab_not_found}, state} end end def handle_manager_call({:switch_tab, tab_id}, _from, state) do case Map.has_key?(state.tabs, tab_id) do true -> updated_state = %{state | active_tab: tab_id} {:reply, {:ok, updated_state}, updated_state} false -> {:reply, {:error, :tab_not_found}, state} end end def handle_manager_call({:get_tab_config, tab_id}, _from, state) do case Map.get(state.tabs, tab_id) do nil -> {:reply, {:error, :tab_not_found}, state} config -> {:reply, {:ok, config}, state} end end def handle_manager_call( {:update_tab_config, tab_id, config_updates}, _from, state ) do case Map.get(state.tabs, tab_id) do nil -> {:reply, {:error, :tab_not_found}, state} current_config -> updated_config = Map.merge(current_config, config_updates) updated_state = %{ state | tabs: Map.put(state.tabs, tab_id, updated_config) } {:reply, {:ok, updated_state}, updated_state} end end def handle_manager_call(:list_tabs, _from, state) do {:reply, state.tabs, state} end def handle_manager_call(:get_active_tab, _from, state) do {:reply, state.active_tab, state} end def handle_manager_call({:set_horizontal_tab, position}, _from, state) do updated_stops = MapSet.put(state.tab_stops, position) updated_state = %{state | tab_stops: updated_stops} {:reply, {:ok, updated_state}, updated_state} end def handle_manager_call({:clear_tab_stop, position}, _from, state) do updated_stops = MapSet.delete(state.tab_stops, position) updated_state = %{state | tab_stops: updated_stops} {:reply, {:ok, updated_state}, updated_state} end def handle_manager_call(:clear_all_tab_stops, _from, state) do updated_state = %{state | tab_stops: MapSet.new()} {:reply, {:ok, updated_state}, updated_state} end def handle_manager_call({:get_next_tab_stop, current_position}, _from, state) do next_stop = find_next_tab_stop(current_position, state.tab_stops) {:reply, next_stop, state} end # Private helper functions defp generate_tab_id(state) do "tab_#{state.next_tab_id}" end defp find_next_tab_stop(current_position, tab_stops) do case Enum.find(tab_stops, fn stop -> stop > current_position end) do # Default tab width nil -> current_position + 8 stop -> stop end end end