diff --git a/public/favicon.ico b/public/favicon.ico index 9dfe8173..862e8505 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 00000000..4a51272d --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,80 @@ + + + + + + + + + + >_ + + + + + + + + + diff --git a/src/apps/Config Editor/ConfigEditorSidebar.tsx b/src/apps/Config Editor/ConfigEditorSidebar.tsx index c3cec7fd..71303884 100644 --- a/src/apps/Config Editor/ConfigEditorSidebar.tsx +++ b/src/apps/Config Editor/ConfigEditorSidebar.tsx @@ -21,6 +21,7 @@ import { import { Separator, } from "@/components/ui/separator.tsx" +import Icon from "../../../public/icon.svg"; interface SidebarProps { onSelectView: (view: string) => void; @@ -32,8 +33,9 @@ export function ConfigEditorSidebar({ onSelectView }: SidebarProps): React.React - - Termix / Config Editor + + Icon + - Termix / Config diff --git a/src/apps/Homepage/HomepageSidebar.tsx b/src/apps/Homepage/HomepageSidebar.tsx index 36deb0f9..81fcdc88 100644 --- a/src/apps/Homepage/HomepageSidebar.tsx +++ b/src/apps/Homepage/HomepageSidebar.tsx @@ -16,6 +16,7 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarProvider, } from "@/components/ui/sidebar.tsx" +import Icon from "/public/icon.svg"; import { Separator, @@ -85,8 +86,9 @@ export function HomepageSidebar({ onSelectView, disabled, isAdmin, username }: S - - Termix + + Icon + - Termix diff --git a/src/apps/SSH Tunnel/SSHTunnelSidebar.tsx b/src/apps/SSH Tunnel/SSHTunnelSidebar.tsx index 7f8c2dc8..86a7f792 100644 --- a/src/apps/SSH Tunnel/SSHTunnelSidebar.tsx +++ b/src/apps/SSH Tunnel/SSHTunnelSidebar.tsx @@ -21,6 +21,7 @@ import { import { Separator, } from "@/components/ui/separator.tsx" +import Icon from "../../../public/icon.svg"; interface SidebarProps { onSelectView: (view: string) => void; @@ -32,8 +33,9 @@ export function SSHTunnelSidebar({ onSelectView }: SidebarProps): React.ReactEle - - Termix / SSH Tunnel + + Icon + - Termix / SSH Tunnel diff --git a/src/apps/SSH/SSH.tsx b/src/apps/SSH/SSH.tsx index fac17ad8..8029ae58 100644 --- a/src/apps/SSH/SSH.tsx +++ b/src/apps/SSH/SSH.tsx @@ -431,6 +431,14 @@ export function SSH({ onSelectView }: ConfigEditorProps): React.ReactElement { onSelectView={onSelectView} onAddHostSubmit={onAddHostSubmit} onHostConnect={onHostConnect} + allTabs={allTabs} + runCommandOnTabs={(tabIds: number[], command: string) => { + allTabs.forEach(tab => { + if (tabIds.includes(tab.id) && tab.terminalRef?.current?.sendInput) { + tab.terminalRef.current.sendInput(command); + } + }); + }} /> {/* Main area: fills the rest */} diff --git a/src/apps/SSH/SSHSidebar.tsx b/src/apps/SSH/SSHSidebar.tsx index cade8ce9..3aeb58fc 100644 --- a/src/apps/SSH/SSHSidebar.tsx +++ b/src/apps/SSH/SSHSidebar.tsx @@ -4,7 +4,8 @@ import { useForm, Controller } from "react-hook-form"; import { CornerDownLeft, Plus, - MoreVertical + MoreVertical, + Hammer } from "lucide-react" import { @@ -58,11 +59,15 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import Icon from "../../../public/icon.svg"; +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; interface SidebarProps { onSelectView: (view: string) => void; onAddHostSubmit: (data: any) => void; onHostConnect: (hostConfig: any) => void; + allTabs: { id: number; title: string; terminalRef: React.RefObject }[]; + runCommandOnTabs: (tabIds: number[], command: string) => void; } interface AuthPromptFormData { @@ -70,6 +75,8 @@ interface AuthPromptFormData { authMethod: string; sshKeyFile: File | null; sshKeyContent?: string; + keyPassword?: string; + keyType?: string; } interface AddHostFormData { @@ -84,11 +91,13 @@ interface AddHostFormData { authMethod: string; sshKeyFile: File | null; sshKeyContent?: string; + keyPassword?: string; + keyType?: string; saveAuthMethod: boolean; isPinned: boolean; } -export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: SidebarProps): React.ReactElement { +export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect, allTabs, runCommandOnTabs }: SidebarProps): React.ReactElement { const addHostForm = useForm({ defaultValues: { name: '', @@ -228,6 +237,8 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid password: data.password, authMethod: data.authMethod, key: sshKeyContent, + keyPassword: data.keyPassword, + keyType: data.keyType === 'auto' ? '' : data.keyType, saveAuthMethod: data.saveAuthMethod, isPinned: data.isPinned }, @@ -381,6 +392,8 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid tagsInput: '', sshKeyFile: null, sshKeyContent: editHostData.key || '', + keyPassword: editHostData.keyPassword || '', + keyType: editHostData.keyType || '', }); } }, [editHostData]); @@ -423,9 +436,11 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid ip: data.ip, port: data.port, username: data.username, - password: data.password, + password: data.password, // always send authMethod: data.authMethod, - key: sshKeyContent, + key: sshKeyContent, // always send + keyPassword: data.keyPassword, // always send + keyType: data.keyType, // always send saveAuthMethod: data.saveAuthMethod, isPinned: data.isPinned }, @@ -486,6 +501,8 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid ...authPromptHost, password: data.authMethod === 'password' ? data.password : undefined, key: data.authMethod === 'key' ? sshKeyContent : undefined, + keyPassword: data.authMethod === 'key' ? data.keyPassword : undefined, + keyType: data.authMethod === 'key' ? (data.keyType === 'auto' ? undefined : data.keyType) : undefined, authMethod: data.authMethod, }; @@ -507,17 +524,125 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid } }, [authPromptOpen, authPromptForm]); + // Key type options + const keyTypeOptions = [ + { value: 'auto', label: 'Auto-detect' }, + { value: 'ssh-rsa', label: 'RSA' }, + { value: 'ssh-ed25519', label: 'ED25519' }, + { value: 'ecdsa-sha2-nistp256', label: 'ECDSA NIST P-256' }, + { value: 'ecdsa-sha2-nistp384', label: 'ECDSA NIST P-384' }, + { value: 'ecdsa-sha2-nistp521', label: 'ECDSA NIST P-521' }, + { value: 'ssh-dss', label: 'DSA' }, + { value: 'ssh-rsa-sha2-256', label: 'RSA SHA2-256' }, + { value: 'ssh-rsa-sha2-512', label: 'RSA SHA2-512' }, + ]; + + const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); + const [editKeyTypeDropdownOpen, setEditKeyTypeDropdownOpen] = useState(false); + const keyTypeDropdownRef = React.useRef(null); + const editKeyTypeDropdownRef = React.useRef(null); + const keyTypeButtonRef = React.useRef(null); + const editKeyTypeButtonRef = React.useRef(null); + + // Close dropdown on outside click (add form) + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + keyTypeDropdownRef.current && + !keyTypeDropdownRef.current.contains(event.target as Node) && + keyTypeButtonRef.current && + !keyTypeButtonRef.current.contains(event.target as Node) + ) { + setKeyTypeDropdownOpen(false); + } + } + if (keyTypeDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [keyTypeDropdownOpen]); + // Close dropdown on outside click (edit form) + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + editKeyTypeDropdownRef.current && + !editKeyTypeDropdownRef.current.contains(event.target as Node) && + editKeyTypeButtonRef.current && + !editKeyTypeButtonRef.current.contains(event.target as Node) + ) { + setEditKeyTypeDropdownOpen(false); + } + } + if (editKeyTypeDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [editKeyTypeDropdownOpen]); + + const [keyTypeDropdownOpenAuth, setKeyTypeDropdownOpenAuth] = useState(false); + const keyTypeDropdownAuthRef = React.useRef(null); + const keyTypeButtonAuthRef = React.useRef(null); + + // Close dropdown on outside click (auth prompt) + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + keyTypeDropdownAuthRef.current && + !keyTypeDropdownAuthRef.current.contains(event.target as Node) && + keyTypeButtonAuthRef.current && + !keyTypeButtonAuthRef.current.contains(event.target as Node) + ) { + setKeyTypeDropdownOpenAuth(false); + } + } + if (keyTypeDropdownOpenAuth) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [keyTypeDropdownOpenAuth]); + + // Tools Sheet State + const [toolsSheetOpen, setToolsSheetOpen] = useState(false); + const [toolsCommand, setToolsCommand] = useState(""); + const [selectedTabIds, setSelectedTabIds] = useState([]); + const handleTabToggle = (tabId: number) => { + setSelectedTabIds(prev => prev.includes(tabId) ? prev.filter(id => id !== tabId) : [...prev, tabId]); + }; + // --- Fix: Run Command logic --- + const handleRunCommand = () => { + if (selectedTabIds.length && toolsCommand.trim()) { + // Ensure command ends with newline + let cmd = toolsCommand; + if (!cmd.endsWith("\n")) cmd += "\n"; + runCommandOnTabs(selectedTabIds, cmd); + setToolsCommand(""); // Clear after run + } + }; + return ( - - - - - Termix / SSH + + + + + Icon + - Termix / SSH - - + + - - - - )} - /> - + + ( + + SSH Private Key + +
+ { + const file = e.target.files?.[0]; + field.onChange(file || null); + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+
+
+ )} + /> + ( + + Key Password (if protected) + + + + + + )} + /> + ( + + Key Type + +
+ + {keyTypeDropdownOpen && ( +
+
+ {keyTypeOptions.map(opt => ( + + ))} +
+
+ )} +
+
+ +
+ )} + /> +
)} /> @@ -865,10 +1054,10 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
- -
+ +
{/* Search bar */} -
+
setSearch(e.target.value)} @@ -890,7 +1079,7 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
)}
- + 0 ? sortedFolders : undefined}> {sortedFolders.map((folder, idx) => ( @@ -921,6 +1110,65 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid + {/* Tools Button at the very bottom */} +
+ + + + + + + Tools + +
+ + + Run multiwindow commands + +