diff --git a/src/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx b/src/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx index c1389b44..961dd717 100644 --- a/src/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx +++ b/src/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx @@ -19,8 +19,21 @@ import { updateSnippet, deleteSnippet, } from "@/ui/main-axios"; +import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"; import type { Snippet, SnippetData } from "../../../../types/index.js"; +interface TabData { + id: number; + type: string; + title: string; + terminalRef?: { + current?: { + sendInput?: (data: string) => void; + }; + }; + [key: string]: unknown; +} + interface SnippetsSidebarProps { isOpen: boolean; onClose: () => void; @@ -34,6 +47,7 @@ export function SnippetsSidebar({ }: SnippetsSidebarProps) { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); + const { tabs } = useTabs() as { tabs: TabData[] }; const [snippets, setSnippets] = useState([]); const [loading, setLoading] = useState(true); const [showDialog, setShowDialog] = useState(false); @@ -47,6 +61,7 @@ export function SnippetsSidebar({ name: false, content: false, }); + const [selectedTabIds, setSelectedTabIds] = useState([]); useEffect(() => { if (isOpen) { @@ -134,9 +149,34 @@ export function SnippetsSidebar({ } }; + const handleTabToggle = (tabId: number) => { + setSelectedTabIds((prev) => + prev.includes(tabId) + ? prev.filter((id) => id !== tabId) + : [...prev, tabId], + ); + }; + const handleExecute = (snippet: Snippet) => { - onExecute(snippet.content); - toast.success(t("snippets.executeSuccess", { name: snippet.name })); + if (selectedTabIds.length > 0) { + // Execute on selected terminals + selectedTabIds.forEach((tabId) => { + const tab = tabs.find((t: TabData) => t.id === tabId); + if (tab?.terminalRef?.current?.sendInput) { + tab.terminalRef.current.sendInput(snippet.content + "\n"); + } + }); + toast.success( + t("snippets.executeSuccess", { + name: snippet.name, + count: selectedTabIds.length, + }), + ); + } else { + // Execute on current terminal (legacy behavior) + onExecute(snippet.content); + toast.success(t("snippets.executeSuccess", { name: snippet.name })); + } }; const handleCopy = (snippet: Snippet) => { @@ -144,6 +184,8 @@ export function SnippetsSidebar({ toast.success(t("snippets.copySuccess", { name: snippet.name })); }; + const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal"); + if (!isOpen) return null; return ( @@ -184,6 +226,49 @@ export function SnippetsSidebar({ {/* Content */}
+ {/* Terminal Selection */} + {terminalTabs.length > 0 && ( + <> +
+ +

+ {selectedTabIds.length > 0 + ? t("snippets.executeOnSelected", { + defaultValue: `Execute on ${selectedTabIds.length} selected terminal(s)`, + count: selectedTabIds.length, + }) + : t("snippets.executeOnCurrent", { + defaultValue: + "Execute on current terminal (click to select multiple)", + })} +

+
+ {terminalTabs.map((tab) => ( + + ))} +
+
+ + + )} + +
+ +
+
+

{t("sshTools.keyRecording")}

+ +
+
+
+ {!isRecording ? ( + + ) : ( + + )} +
+ + {isRecording && ( + <> +
+ +
+ {terminalTabs.map((tab) => ( + + ))} +
+
+ +
+ + +

+ {t("sshTools.commandsWillBeSent", { + count: selectedTabIds.length, + })} +

+
+ + )} +
+
+ + + +

{t("sshTools.settings")}

+ +
+ + +
+ + + +

+ {t("sshTools.shareIdeas")}{" "} + + GitHub + + ! +

+
+
+
+ + ); +} diff --git a/src/ui/Desktop/Apps/Tools/ToolsMenu.tsx b/src/ui/Desktop/Apps/Tools/ToolsMenu.tsx new file mode 100644 index 00000000..5f6bf96e --- /dev/null +++ b/src/ui/Desktop/Apps/Tools/ToolsMenu.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Hammer, Wrench, FileText } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +interface ToolsMenuProps { + onOpenSshTools: () => void; + onOpenSnippets: () => void; +} + +export function ToolsMenu({ + onOpenSshTools, + onOpenSnippets, +}: ToolsMenuProps): React.ReactElement { + const { t } = useTranslation(); + + return ( + + + + + + + + {t("sshTools.title")} + + + + {t("snippets.title")} + + + + ); +} diff --git a/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx b/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx index b5cb5070..fe14b055 100644 --- a/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx +++ b/src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx @@ -68,10 +68,6 @@ export function TabDropdown(): React.ReactElement { setCurrentTab(tabId); }; - if (tabs.length <= 1) { - return null; - } - return ( diff --git a/src/ui/Desktop/Navigation/TopNavbar.tsx b/src/ui/Desktop/Navigation/TopNavbar.tsx index 7f98e168..50bfc0f1 100644 --- a/src/ui/Desktop/Navigation/TopNavbar.tsx +++ b/src/ui/Desktop/Navigation/TopNavbar.tsx @@ -2,16 +2,14 @@ import React, { useState } from "react"; import { flushSync } from "react-dom"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Button } from "@/components/ui/button.tsx"; -import { ChevronDown, ChevronUpIcon, Hammer, FileText } from "lucide-react"; +import { ChevronDown, ChevronUpIcon } from "lucide-react"; import { Tab } from "@/ui/Desktop/Navigation/Tabs/Tab.tsx"; import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"; -import { Input } from "@/components/ui/input.tsx"; -import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { Separator } from "@/components/ui/separator.tsx"; import { useTranslation } from "react-i18next"; import { TabDropdown } from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx"; -import { getCookie, setCookie } from "@/ui/main-axios.ts"; import { SnippetsSidebar } from "@/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx"; +import { SshToolsSidebar } from "@/ui/Desktop/Apps/Tools/SshToolsSidebar.tsx"; +import { ToolsMenu } from "@/ui/Desktop/Apps/Tools/ToolsMenu.tsx"; interface TabData { id: number; @@ -56,8 +54,6 @@ export function TopNavbar({ const { t } = useTranslation(); const [toolsSheetOpen, setToolsSheetOpen] = useState(false); - const [isRecording, setIsRecording] = useState(false); - const [selectedTabIds, setSelectedTabIds] = useState([]); const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false); const [justDroppedTabId, setJustDroppedTabId] = useState(null); const [isInDropAnimation, setIsInDropAnimation] = useState(false); @@ -92,164 +88,6 @@ export function TopNavbar({ removeTab(tabId); }; - const handleTabToggle = (tabId: number) => { - setSelectedTabIds((prev) => - prev.includes(tabId) - ? prev.filter((id) => id !== tabId) - : [...prev, tabId], - ); - }; - - const handleStartRecording = () => { - setIsRecording(true); - setTimeout(() => { - const input = document.getElementById( - "ssh-tools-input", - ) as HTMLInputElement; - if (input) input.focus(); - }, 100); - }; - - const handleStopRecording = () => { - setIsRecording(false); - setSelectedTabIds([]); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (selectedTabIds.length === 0) return; - - let commandToSend = ""; - - if (e.ctrlKey || e.metaKey) { - if (e.key === "c") { - commandToSend = "\x03"; // Ctrl+C (SIGINT) - e.preventDefault(); - } else if (e.key === "d") { - commandToSend = "\x04"; // Ctrl+D (EOF) - e.preventDefault(); - } else if (e.key === "l") { - commandToSend = "\x0c"; // Ctrl+L (clear screen) - e.preventDefault(); - } else if (e.key === "u") { - commandToSend = "\x15"; // Ctrl+U (clear line) - e.preventDefault(); - } else if (e.key === "k") { - commandToSend = "\x0b"; // Ctrl+K (clear from cursor to end) - e.preventDefault(); - } else if (e.key === "a") { - commandToSend = "\x01"; // Ctrl+A (move to beginning of line) - e.preventDefault(); - } else if (e.key === "e") { - commandToSend = "\x05"; // Ctrl+E (move to end of line) - e.preventDefault(); - } else if (e.key === "w") { - commandToSend = "\x17"; // Ctrl+W (delete word before cursor) - e.preventDefault(); - } - } else if (e.key === "Enter") { - commandToSend = "\n"; - e.preventDefault(); - } else if (e.key === "Backspace") { - commandToSend = "\x08"; // Backspace - e.preventDefault(); - } else if (e.key === "Delete") { - commandToSend = "\x7f"; // Delete - e.preventDefault(); - } else if (e.key === "Tab") { - commandToSend = "\x09"; // Tab - e.preventDefault(); - } else if (e.key === "Escape") { - commandToSend = "\x1b"; // Escape - e.preventDefault(); - } else if (e.key === "ArrowUp") { - commandToSend = "\x1b[A"; // Up arrow - e.preventDefault(); - } else if (e.key === "ArrowDown") { - commandToSend = "\x1b[B"; // Down arrow - e.preventDefault(); - } else if (e.key === "ArrowLeft") { - commandToSend = "\x1b[D"; // Left arrow - e.preventDefault(); - } else if (e.key === "ArrowRight") { - commandToSend = "\x1b[C"; // Right arrow - e.preventDefault(); - } else if (e.key === "Home") { - commandToSend = "\x1b[H"; // Home - e.preventDefault(); - } else if (e.key === "End") { - commandToSend = "\x1b[F"; // End - e.preventDefault(); - } else if (e.key === "PageUp") { - commandToSend = "\x1b[5~"; // Page Up - e.preventDefault(); - } else if (e.key === "PageDown") { - commandToSend = "\x1b[6~"; // Page Down - e.preventDefault(); - } else if (e.key === "Insert") { - commandToSend = "\x1b[2~"; // Insert - e.preventDefault(); - } else if (e.key === "F1") { - commandToSend = "\x1bOP"; // F1 - e.preventDefault(); - } else if (e.key === "F2") { - commandToSend = "\x1bOQ"; // F2 - e.preventDefault(); - } else if (e.key === "F3") { - commandToSend = "\x1bOR"; // F3 - e.preventDefault(); - } else if (e.key === "F4") { - commandToSend = "\x1bOS"; // F4 - e.preventDefault(); - } else if (e.key === "F5") { - commandToSend = "\x1b[15~"; // F5 - e.preventDefault(); - } else if (e.key === "F6") { - commandToSend = "\x1b[17~"; // F6 - e.preventDefault(); - } else if (e.key === "F7") { - commandToSend = "\x1b[18~"; // F7 - e.preventDefault(); - } else if (e.key === "F8") { - commandToSend = "\x1b[19~"; // F8 - e.preventDefault(); - } else if (e.key === "F9") { - commandToSend = "\x1b[20~"; // F9 - e.preventDefault(); - } else if (e.key === "F10") { - commandToSend = "\x1b[21~"; // F10 - e.preventDefault(); - } else if (e.key === "F11") { - commandToSend = "\x1b[23~"; // F11 - e.preventDefault(); - } else if (e.key === "F12") { - commandToSend = "\x1b[24~"; // F12 - e.preventDefault(); - } - - if (commandToSend) { - selectedTabIds.forEach((tabId) => { - const tab = tabs.find((t: TabData) => t.id === tabId); - if (tab?.terminalRef?.current?.sendInput) { - tab.terminalRef.current.sendInput(commandToSend); - } - }); - } - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (selectedTabIds.length === 0) return; - - if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { - const char = e.key; - selectedTabIds.forEach((tabId) => { - const tab = tabs.find((t: TabData) => t.id === tabId); - if (tab?.terminalRef?.current?.sendInput) { - tab.terminalRef.current.sendInput(char); - } - }); - } - }; - const handleSnippetExecute = (content: string) => { const tab = tabs.find((t: TabData) => t.id === currentTab); if (tab?.terminalRef?.current?.sendInput) { @@ -467,12 +305,6 @@ export function TopNavbar({ const currentTabIsAdmin = currentTabObj?.type === "admin"; const currentTabIsUserProfile = currentTabObj?.type === "user_profile"; - const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal"); - - const updateRightClickCopyPaste = (checked: boolean) => { - setCookie("rightClickCopyPaste", checked.toString()); - }; - return (
- - - + setToolsSheetOpen(true)} + onOpenSnippets={() => setSnippetsSidebarOpen(true)} + /> -
- -
-
-

{t("sshTools.keyRecording")}

- -
-
-
- {!isRecording ? ( - - ) : ( - - )} -
- - {isRecording && ( - <> -
- -
- {terminalTabs.map((tab) => ( - - ))} -
-
- -
- - -

- {t("sshTools.commandsWillBeSent", { - count: selectedTabIds.length, - })} -

-
- - )} -
-
- - - -

{t("sshTools.settings")}

- -
- - -
- - - -

- {t("sshTools.shareIdeas")}{" "} - - GitHub - - ! -

-
-
-
- - )} + setToolsSheetOpen(false)} + />