v1.8.0 #429

Merged
LukeGus merged 198 commits from dev-1.8.0 into main 2025-11-05 16:36:16 +00:00
5 changed files with 504 additions and 343 deletions
Showing only changes of commit 562d8c96fd - Show all commits

View File

@@ -19,8 +19,21 @@ import {
updateSnippet, updateSnippet,
deleteSnippet, deleteSnippet,
} from "@/ui/main-axios"; } from "@/ui/main-axios";
import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
import type { Snippet, SnippetData } from "../../../../types/index.js"; 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 { interface SnippetsSidebarProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
@@ -34,6 +47,7 @@ export function SnippetsSidebar({
}: SnippetsSidebarProps) { }: SnippetsSidebarProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { confirmWithToast } = useConfirmation(); const { confirmWithToast } = useConfirmation();
const { tabs } = useTabs() as { tabs: TabData[] };
const [snippets, setSnippets] = useState<Snippet[]>([]); const [snippets, setSnippets] = useState<Snippet[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
@@ -47,6 +61,7 @@ export function SnippetsSidebar({
name: false, name: false,
content: false, content: false,
}); });
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
useEffect(() => { useEffect(() => {
if (isOpen) { 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) => { const handleExecute = (snippet: Snippet) => {
onExecute(snippet.content); if (selectedTabIds.length > 0) {
toast.success(t("snippets.executeSuccess", { name: snippet.name })); // 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) => { const handleCopy = (snippet: Snippet) => {
@@ -144,6 +184,8 @@ export function SnippetsSidebar({
toast.success(t("snippets.copySuccess", { name: snippet.name })); toast.success(t("snippets.copySuccess", { name: snippet.name }));
}; };
const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@@ -184,6 +226,49 @@ export function SnippetsSidebar({
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4"> <div className="space-y-4">
{/* Terminal Selection */}
{terminalTabs.length > 0 && (
<>
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("snippets.selectTerminals", {
defaultValue: "Select Terminals (optional)",
})}
</label>
<p className="text-xs text-muted-foreground">
{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)",
})}
</p>
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
{terminalTabs.map((tab) => (
<Button
key={tab.id}
type="button"
variant="outline"
size="sm"
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
selectedTabIds.includes(tab.id)
? "text-white bg-gray-700"
: "text-gray-500"
}`}
onClick={() => handleTabToggle(tab.id)}
>
{tab.title}
</Button>
))}
</div>
</div>
<Separator className="my-4" />
</>
)}
<Button <Button
onClick={handleCreate} onClick={handleCreate}
className="w-full" className="w-full"

View File

@@ -0,0 +1,351 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button.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 { getCookie, setCookie } from "@/ui/main-axios.ts";
import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
import { X } from "lucide-react";
interface TabData {
id: number;
type: string;
title: string;
terminalRef?: {
current?: {
sendInput?: (data: string) => void;
};
};
[key: string]: unknown;
}
interface SshToolsSidebarProps {
isOpen: boolean;
onClose: () => void;
}
export function SshToolsSidebar({
isOpen,
onClose,
}: SshToolsSidebarProps): React.ReactElement | null {
const { t } = useTranslation();
const { tabs } = useTabs() as { tabs: TabData[] };
const [isRecording, setIsRecording] = useState(false);
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 updateRightClickCopyPaste = (checked: boolean) => {
setCookie("rightClickCopyPaste", checked.toString());
};
const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
if (!isOpen) return null;
return (
<div
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] flex justify-end pointer-events-auto isolate"
style={{
transform: "translateZ(0)",
}}
>
<div className="flex-1 cursor-pointer" onClick={onClose} />
<div
className="w-[400px] h-full bg-dark-bg border-l-2 border-dark-border flex flex-col shadow-2xl relative isolate z-[999999]"
style={{
boxShadow: "-4px 0 20px rgba(0, 0, 0, 0.5)",
transform: "translateZ(0)",
}}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-4 border-b border-dark-border">
<h2 className="text-lg font-semibold text-white">
{t("sshTools.title")}
</h2>
<Button
variant="outline"
size="sm"
onClick={onClose}
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-white transition-colors flex items-center justify-center"
title={t("sshTools.closeTools")}
>
<span className="text-lg font-bold leading-none">
<X />
</span>
</Button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
<h1 className="font-semibold">{t("sshTools.keyRecording")}</h1>
<div className="space-y-4">
<div className="space-y-4">
<div className="flex gap-2">
{!isRecording ? (
<Button
onClick={handleStartRecording}
className="flex-1"
variant="outline"
>
{t("sshTools.startKeyRecording")}
</Button>
) : (
<Button
onClick={handleStopRecording}
className="flex-1"
variant="destructive"
>
{t("sshTools.stopKeyRecording")}
</Button>
)}
</div>
{isRecording && (
<>
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("sshTools.selectTerminals")}
</label>
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto mt-2">
{terminalTabs.map((tab) => (
<Button
key={tab.id}
type="button"
variant="outline"
size="sm"
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
selectedTabIds.includes(tab.id)
? "text-white bg-gray-700"
: "text-gray-500"
}`}
onClick={() => handleTabToggle(tab.id)}
>
{tab.title}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("sshTools.typeCommands")}
</label>
<Input
id="ssh-tools-input"
placeholder={t("placeholders.typeHere")}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress}
className="font-mono mt-2"
disabled={selectedTabIds.length === 0}
readOnly
/>
<p className="text-xs text-muted-foreground">
{t("sshTools.commandsWillBeSent", {
count: selectedTabIds.length,
})}
</p>
</div>
</>
)}
</div>
</div>
<Separator className="my-4" />
<h1 className="font-semibold">{t("sshTools.settings")}</h1>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-copy-paste"
onCheckedChange={updateRightClickCopyPaste}
defaultChecked={getCookie("rightClickCopyPaste") === "true"}
/>
<label
htmlFor="enable-copy-paste"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white"
>
{t("sshTools.enableRightClickCopyPaste")}
</label>
</div>
<Separator className="my-4" />
<p className="pt-2 pb-2 text-sm text-gray-500">
{t("sshTools.shareIdeas")}{" "}
<a
href="https://github.com/Termix-SSH/Termix/issues/new"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
GitHub
</a>
!
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="w-[30px] h-[30px] border-dark-border"
title={t("nav.tools")}
>
<Hammer className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-56 bg-dark-bg border-dark-border text-white"
>
<DropdownMenuItem
onClick={onOpenSshTools}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
>
<Wrench className="h-4 w-4" />
<span className="flex-1">{t("sshTools.title")}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={onOpenSnippets}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
>
<FileText className="h-4 w-4" />
<span className="flex-1">{t("snippets.title")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -68,10 +68,6 @@ export function TabDropdown(): React.ReactElement {
setCurrentTab(tabId); setCurrentTab(tabId);
}; };
if (tabs.length <= 1) {
return null;
}
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View File

@@ -2,16 +2,14 @@ import React, { useState } from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useSidebar } from "@/components/ui/sidebar.tsx"; import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Button } from "@/components/ui/button.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 { Tab } from "@/ui/Desktop/Navigation/Tabs/Tab.tsx";
import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.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 { useTranslation } from "react-i18next";
import { TabDropdown } from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx"; 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 { 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 { interface TabData {
id: number; id: number;
@@ -56,8 +54,6 @@ export function TopNavbar({
const { t } = useTranslation(); const { t } = useTranslation();
const [toolsSheetOpen, setToolsSheetOpen] = useState(false); const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false); const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false);
const [justDroppedTabId, setJustDroppedTabId] = useState<number | null>(null); const [justDroppedTabId, setJustDroppedTabId] = useState<number | null>(null);
const [isInDropAnimation, setIsInDropAnimation] = useState(false); const [isInDropAnimation, setIsInDropAnimation] = useState(false);
@@ -92,164 +88,6 @@ export function TopNavbar({
removeTab(tabId); 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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 handleSnippetExecute = (content: string) => {
const tab = tabs.find((t: TabData) => t.id === currentTab); const tab = tabs.find((t: TabData) => t.id === currentTab);
if (tab?.terminalRef?.current?.sendInput) { if (tab?.terminalRef?.current?.sendInput) {
@@ -467,12 +305,6 @@ export function TopNavbar({
const currentTabIsAdmin = currentTabObj?.type === "admin"; const currentTabIsAdmin = currentTabObj?.type === "admin";
const currentTabIsUserProfile = currentTabObj?.type === "user_profile"; const currentTabIsUserProfile = currentTabObj?.type === "user_profile";
const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
const updateRightClickCopyPaste = (checked: boolean) => {
setCookie("rightClickCopyPaste", checked.toString());
};
return ( return (
<div> <div>
<div <div
@@ -648,24 +480,10 @@ export function TopNavbar({
<div className="flex items-center justify-center gap-2 flex-1 px-2"> <div className="flex items-center justify-center gap-2 flex-1 px-2">
<TabDropdown /> <TabDropdown />
<Button <ToolsMenu
variant="outline" onOpenSshTools={() => setToolsSheetOpen(true)}
className="w-[30px] h-[30px]" onOpenSnippets={() => setSnippetsSidebarOpen(true)}
title={t("nav.tools")} />
onClick={() => setToolsSheetOpen(true)}
>
<Hammer className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="w-[30px] h-[30px]"
title={t("nav.snippets")}
onClick={() => setSnippetsSidebarOpen(true)}
disabled={!currentTabObj || currentTabObj.type !== "terminal"}
>
<FileText className="h-4 w-4" />
</Button>
<Button <Button
variant="outline" variant="outline"
@@ -687,154 +505,10 @@ export function TopNavbar({
</div> </div>
)} )}
{toolsSheetOpen && ( <SshToolsSidebar
<div isOpen={toolsSheetOpen}
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] flex justify-end pointer-events-auto isolate" onClose={() => setToolsSheetOpen(false)}
style={{ />
transform: "translateZ(0)",
}}
>
<div
className="flex-1 cursor-pointer"
onClick={() => setToolsSheetOpen(false)}
/>
<div
className="w-[400px] h-full bg-dark-bg border-l-2 border-dark-border flex flex-col shadow-2xl relative isolate z-[999999]"
style={{
boxShadow: "-4px 0 20px rgba(0, 0, 0, 0.5)",
transform: "translateZ(0)",
}}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-4 border-b border-dark-border">
<h2 className="text-lg font-semibold text-white">
{t("sshTools.title")}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => setToolsSheetOpen(false)}
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-white transition-colors flex items-center justify-center"
title={t("sshTools.closeTools")}
>
<span className="text-lg font-bold leading-none">×</span>
</Button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
<h1 className="font-semibold">{t("sshTools.keyRecording")}</h1>
<div className="space-y-4">
<div className="space-y-4">
<div className="flex gap-2">
{!isRecording ? (
<Button
onClick={handleStartRecording}
className="flex-1"
variant="outline"
>
{t("sshTools.startKeyRecording")}
</Button>
) : (
<Button
onClick={handleStopRecording}
className="flex-1"
variant="destructive"
>
{t("sshTools.stopKeyRecording")}
</Button>
)}
</div>
{isRecording && (
<>
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("sshTools.selectTerminals")}
</label>
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto mt-2">
{terminalTabs.map((tab) => (
<Button
key={tab.id}
type="button"
variant="outline"
size="sm"
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
selectedTabIds.includes(tab.id)
? "text-white bg-gray-700"
: "text-gray-500"
}`}
onClick={() => handleTabToggle(tab.id)}
>
{tab.title}
</Button>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white">
{t("sshTools.typeCommands")}
</label>
<Input
id="ssh-tools-input"
placeholder={t("placeholders.typeHere")}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress}
className="font-mono mt-2"
disabled={selectedTabIds.length === 0}
readOnly
/>
<p className="text-xs text-muted-foreground">
{t("sshTools.commandsWillBeSent", {
count: selectedTabIds.length,
})}
</p>
</div>
</>
)}
</div>
</div>
<Separator className="my-4" />
<h1 className="font-semibold">{t("sshTools.settings")}</h1>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-copy-paste"
onCheckedChange={updateRightClickCopyPaste}
defaultChecked={getCookie("rightClickCopyPaste") === "true"}
/>
<label
htmlFor="enable-copy-paste"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-white"
>
{t("sshTools.enableRightClickCopyPaste")}
</label>
</div>
<Separator className="my-4" />
<p className="pt-2 pb-2 text-sm text-gray-500">
{t("sshTools.shareIdeas")}{" "}
<a
href="https://github.com/Termix-SSH/Termix/issues/new"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
GitHub
</a>
!
</p>
</div>
</div>
</div>
</div>
)}
<SnippetsSidebar <SnippetsSidebar
isOpen={snippetsSidebarOpen} isOpen={snippetsSidebarOpen}