Added config editor operations and re-added ssh tools with recording feature
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
import React from "react";
|
||||
import React, {useState} from "react";
|
||||
import {useSidebar} from "@/components/ui/sidebar";
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import {ChevronDown, ChevronUpIcon} from "lucide-react";
|
||||
import {ChevronDown, ChevronUpIcon, Hammer} from "lucide-react";
|
||||
import {Tab} from "@/ui/Navigation/Tabs/Tab.tsx";
|
||||
import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
||||
import {Separator} from "@/components/ui/separator.tsx";
|
||||
|
||||
interface TopNavbarProps {
|
||||
isTopbarOpen: boolean;
|
||||
@@ -15,6 +24,11 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
const {tabs, currentTab, setCurrentTab, setSplitScreenTab, removeTab, allSplitScreenTab} = useTabs() as any;
|
||||
const leftPosition = state === "collapsed" ? "26px" : "264px";
|
||||
|
||||
// SSH Tools state
|
||||
const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
|
||||
|
||||
const handleTabActivate = (tabId: number) => {
|
||||
setCurrentTab(tabId);
|
||||
};
|
||||
@@ -27,12 +41,188 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
removeTab(tabId);
|
||||
};
|
||||
|
||||
const handleTabToggle = (tabId: number) => {
|
||||
setSelectedTabIds(prev => prev.includes(tabId) ? prev.filter(id => id !== tabId) : [...prev, tabId]);
|
||||
};
|
||||
|
||||
const handleStartRecording = () => {
|
||||
setIsRecording(true);
|
||||
// Focus on the input when recording starts
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('ssh-tools-input') as HTMLInputElement;
|
||||
if (input) input.focus();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleStopRecording = () => {
|
||||
setIsRecording(false);
|
||||
setSelectedTabIds([]);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// Don't handle input change for special keys - let onKeyDown handle them
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (selectedTabIds.length === 0) return;
|
||||
|
||||
const value = e.currentTarget.value;
|
||||
let commandToSend = '';
|
||||
|
||||
// Handle special keys and control sequences
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
// Control sequences
|
||||
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();
|
||||
}
|
||||
|
||||
// Send the command to all selected terminals
|
||||
if (commandToSend) {
|
||||
selectedTabIds.forEach(tabId => {
|
||||
const tab = tabs.find((t: any) => t.id === tabId);
|
||||
if (tab?.terminalRef?.current?.sendInput) {
|
||||
tab.terminalRef.current.sendInput(commandToSend);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (selectedTabIds.length === 0) return;
|
||||
|
||||
// Handle regular character input
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
||||
const char = e.key;
|
||||
selectedTabIds.forEach(tabId => {
|
||||
const tab = tabs.find((t: any) => t.id === tabId);
|
||||
if (tab?.terminalRef?.current?.sendInput) {
|
||||
tab.terminalRef.current.sendInput(char);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isSplitScreenActive = Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0;
|
||||
const currentTabObj = tabs.find((t: any) => t.id === currentTab);
|
||||
const currentTabIsHome = currentTabObj?.type === 'home';
|
||||
const currentTabIsSshManager = currentTabObj?.type === 'ssh_manager';
|
||||
const currentTabIsAdmin = currentTabObj?.type === 'admin';
|
||||
|
||||
// Get terminal tabs for selection
|
||||
const terminalTabs = tabs.filter((tab: any) => tab.type === 'terminal');
|
||||
|
||||
function getCookie(name: string) {
|
||||
return document.cookie.split('; ').reduce((r, v) => {
|
||||
const parts = v.split('=');
|
||||
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
||||
}, "");
|
||||
}
|
||||
|
||||
const updateRightClickCopyPaste = (checked: boolean) => {
|
||||
document.cookie = `rightClickCopyPaste=${checked}; expires=2147483647; path=/`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -47,7 +237,7 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
padding: "0"
|
||||
}}
|
||||
>
|
||||
<div className="h-full p-1 pr-2 border-r-2 border-[#303032] w-[calc(100%-3rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
<div className="h-full p-1 pr-2 border-r-2 border-[#303032] w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
{tabs.map((tab: any) => {
|
||||
const isActive = tab.id === currentTab;
|
||||
const isSplit = Array.isArray(allSplitScreenTab) && allSplitScreenTab.includes(tab.id);
|
||||
@@ -82,11 +272,20 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center flex-1">
|
||||
<div className="flex items-center justify-center gap-2 flex-1 px-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-[30px] h-[30px]"
|
||||
title="SSH Tools"
|
||||
onClick={() => setToolsSheetOpen(true)}
|
||||
>
|
||||
<Hammer className="h-4 w-4"/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsTopbarOpen(false)}
|
||||
className="w-[28px] h-[28px]"
|
||||
className="w-[30px] h-[30px]"
|
||||
>
|
||||
<ChevronUpIcon/>
|
||||
</Button>
|
||||
@@ -100,6 +299,165 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
<ChevronDown size={10} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom SSH Tools Overlay */}
|
||||
{toolsSheetOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[999999] flex justify-end"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'auto',
|
||||
isolation: 'isolate',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex-1"
|
||||
onClick={() => setToolsSheetOpen(false)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="w-[400px] h-full bg-[#18181b] border-l-2 border-[#303032] flex flex-col shadow-2xl"
|
||||
style={{
|
||||
backgroundColor: '#18181b',
|
||||
boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 999999,
|
||||
position: 'relative',
|
||||
isolation: 'isolate',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-[#303032]">
|
||||
<h2 className="text-lg font-semibold text-white">SSH Tools</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="Close SSH Tools"
|
||||
>
|
||||
<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">
|
||||
Key Recording
|
||||
</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"
|
||||
>
|
||||
Start Key Recording
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleStopRecording}
|
||||
className="flex-1"
|
||||
variant="destructive"
|
||||
>
|
||||
Stop Key Recording
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isRecording && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">Select terminals:</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)
|
||||
? 'bg-blue-600 text-white border-blue-700 hover:bg-blue-700'
|
||||
: 'bg-transparent text-gray-300 border-gray-500 hover:bg-gray-700'
|
||||
}`}
|
||||
onClick={() => handleTabToggle(tab.id)}
|
||||
>
|
||||
{tab.title}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">Type commands (all keys supported):</label>
|
||||
<Input
|
||||
id="ssh-tools-input"
|
||||
placeholder="Type here"
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="font-mono mt-2"
|
||||
disabled={selectedTabIds.length === 0}
|
||||
readOnly
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Commands will be sent to {selectedTabIds.length} selected terminal(s).
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4"/>
|
||||
|
||||
<h1 className="font-semibold">
|
||||
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"
|
||||
>
|
||||
Enable right‑click copy/paste
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4"/>
|
||||
|
||||
<p className="pt-2 pb-2 text-sm text-gray-500">
|
||||
Have ideas for what should come next for ssh tools? Share them on{" "}
|
||||
<a
|
||||
href="https://github.com/LukeGus/Termix/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user