diff --git a/AppImage/components/lxc-terminal-modal.tsx b/AppImage/components/lxc-terminal-modal.tsx index 674662b7..b0e5c705 100644 --- a/AppImage/components/lxc-terminal-modal.tsx +++ b/AppImage/components/lxc-terminal-modal.tsx @@ -13,6 +13,12 @@ import { CornerDownLeft, GripHorizontal, ChevronDown, + Search, + Send, + Lightbulb, + Terminal, + Trash2, + X, } from "lucide-react" import { DropdownMenu, @@ -22,8 +28,11 @@ import { DropdownMenuSeparator, DropdownMenuLabel, } from "@/components/ui/dropdown-menu" +import { DialogHeader, DialogDescription } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Dialog as SearchDialog, DialogContent as SearchDialogContent, DialogTitle as SearchDialogTitle } from "@/components/ui/dialog" import "xterm/css/xterm.css" -import { API_PORT } from "@/lib/api-config" +import { API_PORT, fetchApi } from "@/lib/api-config" interface LxcTerminalModalProps { open: boolean @@ -32,6 +41,40 @@ interface LxcTerminalModalProps { vmName: string } +interface CheatSheetResult { + command: string + description: string + examples: string[] +} + +const proxmoxCommands = [ + { cmd: "ls -la", desc: "List all files with details" }, + { cmd: "cd /path/to/dir", desc: "Change directory" }, + { cmd: "cat filename", desc: "Display file contents" }, + { cmd: "grep 'pattern' file", desc: "Search for pattern in file" }, + { cmd: "find . -name 'file'", desc: "Find files by name" }, + { cmd: "df -h", desc: "Show disk usage" }, + { cmd: "du -sh *", desc: "Show directory sizes" }, + { cmd: "free -h", desc: "Show memory usage" }, + { cmd: "top", desc: "Show running processes" }, + { cmd: "ps aux | grep process", desc: "Find running process" }, + { cmd: "systemctl status service", desc: "Check service status" }, + { cmd: "systemctl restart service", desc: "Restart a service" }, + { cmd: "apt update && apt upgrade", desc: "Update packages" }, + { cmd: "apt install package", desc: "Install package" }, + { cmd: "tail -f /var/log/syslog", desc: "Follow log file" }, + { cmd: "chmod 755 file", desc: "Change file permissions" }, + { cmd: "chown user:group file", desc: "Change file owner" }, + { cmd: "tar -xzf file.tar.gz", desc: "Extract tar.gz archive" }, + { cmd: "docker ps", desc: "List running containers" }, + { cmd: "docker images", desc: "List Docker images" }, + { cmd: "ip addr show", desc: "Show IP addresses" }, + { cmd: "ping host", desc: "Test network connectivity" }, + { cmd: "curl -I url", desc: "Get HTTP headers" }, + { cmd: "history", desc: "Show command history" }, + { cmd: "clear", desc: "Clear terminal screen" }, +] + function getWebSocketUrl(): string { if (typeof window === "undefined") { return "ws://localhost:8008/ws/terminal" @@ -71,6 +114,14 @@ export function LxcTerminalModal({ const resizeBarRef = useRef(null) const modalHeightRef = useRef(500) + // Search state + const [searchModalOpen, setSearchModalOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [filteredCommands, setFilteredCommands] = useState>(proxmoxCommands) + const [isSearching, setIsSearching] = useState(false) + const [searchResults, setSearchResults] = useState([]) + const [useOnline, setUseOnline] = useState(true) + // Detect mobile/tablet @@ -358,6 +409,76 @@ export function LxcTerminalModal({ const sendEnter = useCallback(() => sendKey("\r"), [sendKey]) const sendCtrlC = useCallback(() => sendKey("\x03"), [sendKey]) // Ctrl+C + // Search effect - debounced search with cheat.sh + useEffect(() => { + const searchCheatSh = async (query: string) => { + if (!query.trim()) { + setSearchResults([]) + setFilteredCommands(proxmoxCommands) + return + } + + try { + setIsSearching(true) + const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}` + const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, { + method: "GET", + signal: AbortSignal.timeout(10000), + }) + + if (!data.success || !data.examples || data.examples.length === 0) { + throw new Error("No examples found") + } + + const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({ + command: example.command, + description: example.description || "", + examples: [example.command], + })) + + setUseOnline(true) + setSearchResults(formattedResults) + } catch (error) { + const filtered = proxmoxCommands.filter( + (item) => + item.cmd.toLowerCase().includes(query.toLowerCase()) || + item.desc.toLowerCase().includes(query.toLowerCase()), + ) + setFilteredCommands(filtered) + setSearchResults([]) + setUseOnline(false) + } finally { + setIsSearching(false) + } + } + + const debounce = setTimeout(() => { + if (searchQuery && searchQuery.length >= 2) { + searchCheatSh(searchQuery) + } else { + setSearchResults([]) + setFilteredCommands(proxmoxCommands) + } + }, 800) + + return () => clearTimeout(debounce) + }, [searchQuery]) + + const handleClear = useCallback(() => { + if (termRef.current) { + termRef.current.clear() + } + }, []) + + const sendToTerminal = useCallback((command: string) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(command) + setTimeout(() => { + setSearchModalOpen(false) + }, 100) + } + }, []) + const showMobileControls = isMobile || isTablet return ( @@ -382,6 +503,28 @@ export function LxcTerminalModal({ Terminal: {vmName} (ID: {vmid}) +
+ + +
{/* Terminal container */} @@ -504,12 +647,185 @@ export function LxcTerminalModal({ + + {/* Search Commands Modal */} + + + + Search Commands +
+
+
+ + + Search for Linux commands + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base" + autoCapitalize="none" + autoComplete="off" + autoCorrect="off" + spellCheck={false} + /> +
+ + {isSearching && ( +
+
+

Searching cheat.sh...

+
+ )} + +
+ {searchResults.length > 0 ? ( + <> + {searchResults.map((result, index) => ( +
+ {result.description && ( +

# {result.description}

+ )} +
sendToTerminal(result.command)} + className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2" + > + {result.command} + +
+
+ ))} +
+

+ + Powered by cheat.sh +

+
+ + ) : filteredCommands.length > 0 && !useOnline ? ( + filteredCommands.map((item, index) => ( +
sendToTerminal(item.cmd)} + className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" + > +
+
+ {item.cmd} +

{item.desc}

+
+ +
+
+ )) + ) : !isSearching && !searchQuery && !useOnline ? ( + proxmoxCommands.map((item, index) => ( +
sendToTerminal(item.cmd)} + className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" + > +
+
+ {item.cmd} +

{item.desc}

+
+ +
+
+ )) + ) : !isSearching ? ( +
+ {searchQuery ? ( + <> + +
+

{"No results found for \""}{searchQuery}{"\""}

+

Try a different command or check your spelling

+
+ + ) : ( + <> + +
+

Search for any command

+
+

Try searching for:

+
+ {["tar", "grep", "docker", "systemctl", "curl"].map((cmd) => ( + setSearchQuery(cmd)} + className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700" + > + {cmd} + + ))} +
+
+
+ {useOnline && ( +
+ + Powered by cheat.sh +
+ )} + + )} +
+ ) : null} +
+ +
+
+ + Tip: Search for any Linux command +
+ {useOnline && searchResults.length > 0 && Powered by cheat.sh} +
+
+ + ) } diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index fbdd9161..4eec7287 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -1400,6 +1400,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
+ {/* Divider */} +
+ {/* Backup List */}
Available backups