/* eslint-disable react-refresh/only-export-components */ import React, { createContext, useContext, useState, useEffect, useRef, useCallback, useMemo, type ReactNode, } from "react"; import { useTranslation } from "react-i18next"; import type { TabContextTab, TerminalRefHandle } from "@/types/index"; export type Tab = TabContextTab; interface TabContextType { tabs: Tab[]; currentTab: number | null; allSplitScreenTab: number[]; addTab: (tab: Omit) => number; removeTab: (tabId: number) => void; setCurrentTab: (tabId: number) => void; setSplitScreenTab: (tabId: number) => void; getTab: (tabId: number) => Tab | undefined; reorderTabs: (fromIndex: number, toIndex: number) => void; updateHostConfig: ( hostId: number, newHostConfig: { id: number; name?: string; username: string; ip: string; port: number; }, ) => void; updateTab: (tabId: number, updates: Partial>) => void; previewTerminalTheme: string | null; setPreviewTerminalTheme: (theme: string | null) => void; } const TabContext = createContext(undefined); export function useTabs() { const context = useContext(TabContext); if (context === undefined) { throw new Error("useTabs must be used within a TabProvider"); } return context; } const NOOP_TABS: TabContextType = { tabs: [], currentTab: null, allSplitScreenTab: [], addTab: () => -1, removeTab: () => {}, setCurrentTab: () => {}, setSplitScreenTab: () => {}, getTab: () => undefined, reorderTabs: () => {}, updateHostConfig: () => {}, updateTab: () => {}, previewTerminalTheme: null, setPreviewTerminalTheme: () => {}, }; export function useTabsSafe(): TabContextType { return useContext(TabContext) ?? NOOP_TABS; } interface TabProviderProps { children: ReactNode; } export function clearTermixSessionStorage() { localStorage.removeItem("termix_tabs"); localStorage.removeItem("termix_currentTab"); } export function TabProvider({ children }: TabProviderProps) { const { t } = useTranslation(); const [tabs, setTabs] = useState([ { id: 1, type: "home", title: "Home" }, ]); const [currentTab, setCurrentTab] = useState(1); const [allSplitScreenTab, setAllSplitScreenTab] = useState([]); const [previewTerminalTheme, setPreviewTerminalTheme] = useState< string | null >(null); const [initialMaxId] = useState(2); const nextTabId = useRef(initialMaxId); // Safety net: if currentTab points to a tab that no longer exists, fall back to home useEffect(() => { if (tabs.length > 0 && !tabs.some((t) => t.id === currentTab)) { setCurrentTab(1); } }, [tabs, currentTab]); React.useEffect(() => { setTabs((prev) => prev.map((tab) => tab.id === 1 && tab.type === "home" ? { ...tab, title: t("nav.home") } : tab, ), ); }, [t]); const computeUniqueTitle = useCallback( (tabType: Tab["type"], desiredTitle: string | undefined): string => { const defaultTitle = tabType === "server_stats" ? t("nav.hostMetrics") : tabType === "file_manager" ? t("nav.fileManager") : tabType === "tunnel" ? t("nav.tunnels") : tabType === "docker" ? t("nav.docker") : t("nav.terminal"); const baseTitle = (desiredTitle || defaultTitle).trim(); const match = baseTitle.match(/^(.*) \((\d+)\)$/); const root = match ? match[1] : baseTitle; const usedNumbers = new Set(); let rootUsed = false; tabs.forEach((t) => { if (!t.title) return; if (t.title === root) { rootUsed = true; return; } const m = t.title.match( new RegExp( `^${root.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")} \\((\\d+)\\)$`, ), ); if (m) { const n = parseInt(m[1], 10); if (!isNaN(n)) usedNumbers.add(n); } }); if (!rootUsed) return root; let n = 2; while (usedNumbers.has(n)) n += 1; return `${root} (${n})`; }, [t, tabs], ); const addTab = useCallback( (tabData: Omit): number => { // --- tmux-monitor --- (tmux_monitor is a singleton tab like ssh_manager: // re-opening focuses the existing tab instead of adding a duplicate) if (tabData.type === "ssh_manager" || tabData.type === "tmux_monitor") { const existingTab = tabs.find((t) => t.type === tabData.type); if (existingTab) { setTabs((prev) => prev.map((t) => t.id === existingTab.id ? { ...t, title: existingTab.title, hostConfig: tabData.hostConfig ? { ...tabData.hostConfig } : undefined, initialTab: tabData.initialTab, _updateTimestamp: Date.now(), } : t, ), ); setCurrentTab(existingTab.id); setAllSplitScreenTab((prev) => prev.filter((tid) => tid !== existingTab.id), ); return existingTab.id; } } const id = nextTabId.current++; const instanceId = `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const needsUniqueTitle = tabData.type === "terminal" || tabData.type === "server_stats" || tabData.type === "file_manager" || tabData.type === "tunnel" || tabData.type === "docker"; const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : tabData.type === "tmux_monitor" // --- tmux-monitor --- ? tabData.title || t("nav.tmuxMonitor") : tabData.title || ""; const newTab: Tab = { ...tabData, id, instanceId, title: effectiveTitle, terminalRef: tabData.type === "terminal" ? React.createRef() : undefined, hostConfig: tabData.hostConfig ? { ...tabData.hostConfig, instanceId, } : undefined, }; setTabs((prev) => [...prev, newTab]); setCurrentTab(id); setAllSplitScreenTab((prev) => prev.filter((tid) => tid !== id)); return id; }, [computeUniqueTitle, tabs, t], // --- tmux-monitor --- (added t) ); const pendingCurrentTabRef = useRef(null); const removeTab = useCallback( (tabId: number) => { const tab = tabs.find((t) => t.id === tabId); if ( tab && tab.terminalRef?.current && typeof tab.terminalRef.current.disconnect === "function" ) { tab.terminalRef.current.disconnect(); } setTabs((prev) => { const closedIndex = prev.findIndex((t) => t.id === tabId); const filtered = prev.filter((t) => t.id !== tabId); if (filtered.length === 0) { pendingCurrentTabRef.current = 1; return [{ id: 1, type: "home", title: t("nav.home") }]; } // If the closed tab was active, compute the next tab to activate // using the latest prev so rapid closes don't use stale data const nextIndex = closedIndex < filtered.length ? closedIndex : filtered.length - 1; pendingCurrentTabRef.current = filtered[Math.max(0, nextIndex)]?.id ?? 1; return filtered; }); setAllSplitScreenTab((prev) => { const newSplits = prev.filter((id) => id !== tabId); return newSplits.length <= 1 ? [] : newSplits; }); setCurrentTab((prevCurrentTab) => { if (prevCurrentTab !== tabId) return prevCurrentTab; return pendingCurrentTabRef.current ?? 1; }); }, [t, tabs], ); const setSplitScreenTab = useCallback((tabId: number) => { setAllSplitScreenTab((prev) => { if (prev.includes(tabId)) { return prev.filter((id) => id !== tabId); } else if (prev.length < 6) { return [...prev, tabId]; } return prev; }); }, []); const getTab = useCallback( (tabId: number) => { return tabs.find((tab) => tab.id === tabId); }, [tabs], ); const isReorderingRef = useRef(false); const reorderTabs = useCallback((fromIndex: number, toIndex: number) => { if (isReorderingRef.current) return; isReorderingRef.current = true; setTabs((prev) => { const newTabs = [...prev]; const [movedTab] = newTabs.splice(fromIndex, 1); const maxIndex = newTabs.length; const safeToIndex = Math.min(toIndex, maxIndex); newTabs.splice(safeToIndex, 0, movedTab); setTimeout(() => { isReorderingRef.current = false; }, 100); return newTabs; }); }, []); const updateHostConfig = useCallback( ( hostId: number, newHostConfig: { id: number; name?: string; username: string; ip: string; port: number; }, ) => { setTabs((prev) => prev.map((tab) => { if (tab.hostConfig && tab.hostConfig.id === hostId) { if (tab.type === "ssh_manager") { return { ...tab, hostConfig: { ...tab.hostConfig, ...newHostConfig, instanceId: tab.hostConfig.instanceId, }, }; } return { ...tab, hostConfig: { ...tab.hostConfig, ...newHostConfig, instanceId: tab.hostConfig.instanceId, }, title: newHostConfig.name?.trim() ? newHostConfig.name : t("nav.hostTabTitle", { username: newHostConfig.username, ip: newHostConfig.ip, port: newHostConfig.port, }), }; } return tab; }), ); }, [t], ); const updateTab = useCallback( (tabId: number, updates: Partial>) => { setTabs((prev) => prev.map((tab) => tab.id === tabId ? { ...tab, ...updates, _updateTimestamp: Date.now() } : tab, ), ); }, [], ); const value: TabContextType = useMemo( () => ({ tabs, currentTab, allSplitScreenTab, addTab, removeTab, setCurrentTab, setSplitScreenTab, getTab, reorderTabs, updateHostConfig, updateTab, previewTerminalTheme, setPreviewTerminalTheme, }), [ tabs, currentTab, allSplitScreenTab, addTab, removeTab, setSplitScreenTab, getTab, reorderTabs, updateHostConfig, updateTab, previewTerminalTheme, setPreviewTerminalTheme, ], ); return {children}; }