Source: hooks/useControls.js

import React from "react";
import { useContainerActions } from "./creation/useContainerActions";
import { useContainerCreation } from "./creation/useContainerCreation";
import { useLogsViewer } from "./creation/useLogsViewer";
import { useInput } from "ink";
import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
import { EXIT_DELAY } from "../helpers/constants";
import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";

// Principal hook to manage user inputs and control the app state
/**
 * Main hook that wires user input, creation, actions and logs viewing.
 * It coordinates the modular hooks and exposes a compact API consumed by the App.
 *
 * @param {Array<Object>} containers - Current list of Docker containers
 * @returns {Object} controls - API for the App component
 * @property {number} selected - Index of the currently selected container
 * @property {function} setSelected - Setter for selected index
 * @property {string} message - Current feedback message (creation or actions)
 * @property {string} messageColor - Color to show for the feedback message
 * @property {boolean} showLogs - Whether the logs viewer is active
 * @property {Array<string>} logs - Array of log lines currently collected
 * @property {function} exitLogs - Helper to close the logs viewer
 * @property {boolean} creatingContainer - Whether the create-container prompt is open
 * @property {number} creationStep - Current step in the creation flow
 * @property {string} imageNameInput - Current value of the image name field
 * @property {string} containerNameInput - Current value of the container name field
 * @property {string} portInput - Current value of the port input field
 * @property {string} envInput - Current value of the env input field
 * @property {Object} creation - The creation hook API (setters and helpers)
 * @property {Object} actions - The actions hook API (helpers to start/stop/remove)
 * @property {Object} logsViewer - The logs viewer hook API
 */
export function useControls(containers = []) {
  const [selected, setSelected] = React.useState(0);
  const [creatingContainer, setCreatingContainer] = React.useState(false);
  const [confirmErase, setConfirmErase] = React.useState(false);
  const total = containers.length;

  // Modular hooks
  const actions = useContainerActions({ containers });
  const creation = useContainerCreation({
    onCreate: async ({ imageName, containerName, portInput, envInput }) => {
      // Build Docker options
      const env = (envInput || "").split(",").map(s => s.trim()).filter(Boolean);
      const ports = (portInput || "").split(",").map(s => s.trim()).filter(Boolean);
      const ExposedPorts = {};
      const PortBindings = {};
      ports.forEach(pair => {
        const [host, cont] = pair.split(":");
        if (!host || !cont) return;
        const key = `${cont}/tcp`;
        ExposedPorts[key] = {};
        PortBindings[key] = PortBindings[key] || [];
        PortBindings[key].push({ HostPort: `${host}` });
      });

      const options = {
        Tty: true,
      };
      if (Object.keys(ExposedPorts).length) options.ExposedPorts = ExposedPorts;
      if (Object.keys(PortBindings).length) options.HostConfig = { PortBindings };
      if (env.length) options.Env = env;
      if (containerName) options.name = containerName;

  actions.setMessage(`Creating container ${imageName}...`);
  actions.setMessageColor("yellow");
      try {
        const id = await svcCreateContainer(imageName, options);
        actions.setMessage(`Created container ${id}`);
        actions.setMessageColor("green");
      } catch (err) {
        actions.setMessage(`Error creating container: ${err.message}`);
        actions.setMessageColor("red");
      } finally {
        setCreatingContainer(false);
      }
    },
    onCancel: () => setCreatingContainer(false),
    dbImages: ["mysql", "mariadb", "postgres", "mongo", "mssql", "redis"]
  });
  const logsViewer = useLogsViewer();

  // Handler to exit logs (delegated to logsViewer)
  const exitLogs = logsViewer.closeLogs;

  useInput((input, key) => {
  // Erase confirmation
    if (confirmErase) {
      if (input === "y" || input === "Y") {
        actions.handleAction({
          actionFn: async (id) => await actions.removeContainer(id),
          actionLabel: "Erasing",
          selected,
        });
        setConfirmErase(false);
        actions.setMessageColor("yellow");
        return;
      } else if (input === "n" || input === "N" || key.escape) {
        setConfirmErase(false);
        actions.setMessage("");
        actions.setMessageColor("");
        return;
      } else {
        actions.setMessage("Are you sure you want to erase this container? (y/n)");
        actions.setMessageColor("yellow");
        return;
      }
    }

    // Logs viewer
    if (logsViewer.showLogs) {
      if (input === "q" || key.escape) {
        logsViewer.closeLogs();
      }
      return;
    }

    // Container creation flow: delegate input to creation hook
    if (creatingContainer) {
      const step = creation.step;
      const appendCharToField = (setter, value, ch) => {
        setter((value || "") + ch);
      };
      // Escape -> cancel
      if (key.escape) {
        creation.cancelCreation();
        setCreatingContainer(false);
        return;
      }
      // Enter -> next step
      if (input === "\r" || input === "\n") {
        creation.nextStep();
        return;
      }
      // Backspace/Delete support
      if (key.backspace || key.delete) {
        if (step === 0) creation.setImageName((v) => (v || "").slice(0, -1));
        if (step === 1) creation.setContainerName((v) => (v || "").slice(0, -1));
        if (step === 2) creation.setPortInput((v) => (v || "").slice(0, -1));
        if (step === 3) creation.setEnvInput((v) => (v || "").slice(0, -1));
        return;
      }
      // Printable character input (append)
      if (input && input.length === 1 && !key.ctrl && !key.meta) {
        if (step === 0) appendCharToField(creation.setImageName, creation.imageName, input);
        if (step === 1) appendCharToField(creation.setContainerName, creation.containerName, input);
        if (step === 2) appendCharToField(creation.setPortInput, creation.portInput, input);
        if (step === 3) appendCharToField(creation.setEnvInput, creation.envInput, input);
        return;
      }
      // Otherwise ignore
      return;
    }

    //==========================================================
    // Menu Navigation
    //==========================================================
    if (key.upArrow && total > 0) setSelected((i) => (i === 0 ? total - 1 : i - 1));
    if (key.downArrow && total > 0) setSelected((i) => (i === total - 1 ? 0 : i + 1));
    if (input === "q") {
      actions.setMessage("Exiting...");
      actions.setMessageColor("yellow");
      setTimeout(() => process.exit(0), EXIT_DELAY);
      return;
    }

    //==========================================================
    // Docker commands
    //==========================================================
    if (input === "i") {
      actions.handleAction({
        actionFn: async (id) => await actions.startContainer(id),
        actionLabel: "Starting",
        selected,
        stateCheck: (c) => (c.state === "running" || c.status === "running") && "Container is already running."
      });
    }
    if (input === "p") {
      actions.handleAction({
        actionFn: async (id) => await actions.stopContainer(id),
        actionLabel: "Stopping",
        selected,
        stateCheck: (c) => (c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped") && "Container is already stopped."
      });
    }
    if (input === "r") {
      actions.handleAction({
        actionFn: async (id) => await actions.restartContainer(id),
        actionLabel: "Restarting",
        selected,
      });
    }
    if (input === "e" && containers[selected]) {
      setConfirmErase(true);
      actions.setMessage("Are you sure you want to erase this container? (y/n)");
      actions.setMessageColor("yellow");
      return;
    }
    if (input === "l" && containers[selected]) {
      logsViewer.openLogs();
      getLogsStream(
        containers[selected].id,
        (data) => logsViewer.setLogs((prev) => {
          const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
          // Limit to last 1000 lines to prevent memory leak
          return newLogs.slice(-1000);
        }),
        () => {},
        (err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
      );
      return;
    }
    if (input === "c") {
      setCreatingContainer(true);
      creation.setStep(0);
      creation.setImageName("");
      creation.setContainerName("");
      creation.setPortInput("");
      creation.setEnvInput("");
      creation.setMessage("Insert the name of the image to create: ");
      creation.setMessageColor("yellow");
    }
  });

  return {
    selected,
    setSelected,
    // Map messaging to creation when creating, otherwise to actions
    message: creatingContainer ? creation.message : actions.message,
    messageColor: creatingContainer ? creation.messageColor : actions.messageColor,
    showLogs: logsViewer.showLogs,
    logs: logsViewer.logs,
    exitLogs,
    creatingContainer,
    // Expose creation fields in the shape App expects
    creationStep: creation.step,
    imageNameInput: creation.imageName,
    containerNameInput: creation.containerName,
    portInput: creation.portInput,
    envInput: creation.envInput,
    creation,
    actions,
    logsViewer,
    confirmErase,
  };
}