Rename files, improve keyboard usability
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -73,6 +73,7 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
"zod": "^4.0.5"
|
"zod": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -10830,6 +10831,13 @@
|
|||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yargs-parser": {
|
"node_modules/yargs-parser": {
|
||||||
"version": "21.1.1",
|
"version": "21.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
"zod": "^4.0.5"
|
"zod": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const overlayTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const DEBOUNCE_MS = 140;
|
const DEBOUNCE_MS = 140;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -111,7 +112,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
};
|
};
|
||||||
|
|
||||||
textarea.addEventListener('focus', preventKeyboard);
|
textarea.addEventListener('focus', preventKeyboard);
|
||||||
textarea.blur(); // Initial blur
|
textarea.blur();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
textarea.removeEventListener('focus', preventKeyboard);
|
textarea.removeEventListener('focus', preventKeyboard);
|
||||||
@@ -119,6 +120,27 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
}
|
}
|
||||||
}, [terminal]);
|
}, [terminal]);
|
||||||
|
|
||||||
|
function syncOverlay() {
|
||||||
|
if (!terminal || !overlayTextareaRef.current) return;
|
||||||
|
const buffer = terminal.buffer.active;
|
||||||
|
let text = "";
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
text += buffer.getLine(i)?.translateToString() + "\n";
|
||||||
|
}
|
||||||
|
overlayTextareaRef.current.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!terminal) return;
|
||||||
|
syncOverlay();
|
||||||
|
|
||||||
|
const disposeRender = terminal.onRender(() => syncOverlay());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposeRender.dispose();
|
||||||
|
};
|
||||||
|
}, [terminal]);
|
||||||
|
|
||||||
function handleWindowResize() {
|
function handleWindowResize() {
|
||||||
if (!isVisibleRef.current) return;
|
if (!isVisibleRef.current) return;
|
||||||
fitAddonRef.current?.fit();
|
fitAddonRef.current?.fit();
|
||||||
@@ -169,7 +191,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
if (!terminal || !xtermRef.current || !hostConfig) return;
|
if (!terminal || !xtermRef.current || !hostConfig) return;
|
||||||
|
|
||||||
terminal.options = {
|
terminal.options = {
|
||||||
cursorBlink: true,
|
cursorBlink: false,
|
||||||
cursorStyle: 'bar',
|
cursorStyle: 'bar',
|
||||||
scrollback: 10000,
|
scrollback: 10000,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -184,6 +206,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
fastScrollModifier: 'alt',
|
fastScrollModifier: 'alt',
|
||||||
fastScrollSensitivity: 5,
|
fastScrollSensitivity: 5,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
|
disableStdin: true,
|
||||||
|
cursorInactiveStyle: "bar",
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
@@ -276,10 +300,26 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={xtermRef}
|
className="h-full w-full m-1 relative"
|
||||||
className="h-full w-full m-1"
|
style={{ opacity: visible && isVisible ? 1 : 0 }}
|
||||||
style={{opacity: visible && isVisible ? 1 : 0, overflow: 'hidden'}}
|
>
|
||||||
|
<div ref={xtermRef} className="h-full w-full" />
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref={overlayTextareaRef}
|
||||||
|
readOnly
|
||||||
|
className="absolute top-0 left-0 w-full h-full"
|
||||||
|
style={{
|
||||||
|
opacity: 0.01,
|
||||||
|
cursor: "text",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
resize: "none",
|
||||||
|
userSelect: "text",
|
||||||
|
WebkitUserSelect: "text",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
class: "hg-space-small",
|
class: "hg-space-small",
|
||||||
buttons: "{hide} {less} {more}",
|
buttons: "{hide} {unhide} {less} {more}",
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
|
|||||||
"{esc} {tab} {ctrl} {alt} {end} {home} {pgUp} {pgDn}",
|
"{esc} {tab} {ctrl} {alt} {end} {home} {pgUp} {pgDn}",
|
||||||
"1 2 3 4 5 6 7 8 9 0",
|
"1 2 3 4 5 6 7 8 9 0",
|
||||||
"! @ # $ % ^ & * ( ) _ +",
|
"! @ # $ % ^ & * ( ) _ +",
|
||||||
"[ ] { } | \ \ ; : ' \" , . / < >",
|
"[ ] { } | \\ ; : ' \" , . / < >",
|
||||||
"F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
"F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
|
||||||
"{arrowLeft} {arrowRight} {arrowUp} {arrowDown} {paste} {backspace}",
|
"{arrowLeft} {arrowRight} {arrowUp} {arrowDown} {paste} {backspace}",
|
||||||
"{hide} {less} {space} {enter}",
|
"{hide} {less} {space} {enter}",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hg-space-medium {
|
.hg-space-medium {
|
||||||
width: 60px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hg-space-small {
|
.hg-space-small {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, {useRef, FC, useState, useEffect} from "react";
|
import React, {useRef, FC, useState, useEffect} from "react";
|
||||||
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
|
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
|
||||||
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
|
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
|
||||||
import {BottomNavbar} from "@/ui/Mobile/Apps/Navigation/BottomNavbar.tsx";
|
import {BottomNavbar} from "@/ui/Mobile/Navigation/BottomNavbar.tsx";
|
||||||
import {LeftSidebar} from "@/ui/Mobile/Apps/Navigation/LeftSidebar.tsx";
|
import {LeftSidebar} from "@/ui/Mobile/Navigation/LeftSidebar.tsx";
|
||||||
import {TabProvider, useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx";
|
import {TabProvider, useTabs} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
|
||||||
import {getUserInfo} from "@/ui/main-axios.ts";
|
import {getUserInfo} from "@/ui/main-axios.ts";
|
||||||
import {HomepageAuth} from "@/ui/Mobile/Homepage/HomepageAuth.tsx";
|
import {HomepageAuth} from "@/ui/Mobile/Homepage/HomepageAuth.tsx";
|
||||||
|
|
||||||
@@ -58,6 +58,14 @@ const AppContent: FC = () => {
|
|||||||
return () => window.removeEventListener('storage', handleStorageChange)
|
return () => window.removeEventListener('storage', handleStorageChange)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fitCurrentTerminal()
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAuthSuccess = (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => {
|
const handleAuthSuccess = (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => {
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
setIsAdmin(authData.isAdmin)
|
setIsAdmin(authData.isAdmin)
|
||||||
@@ -145,8 +153,11 @@ const AppContent: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{currentTab && <TerminalKeyboard onSendInput={handleKeyboardInput}/>}
|
{currentTab &&
|
||||||
|
<div className="mb-1">
|
||||||
|
<TerminalKeyboard onSendInput={handleKeyboardInput}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<BottomNavbar
|
<BottomNavbar
|
||||||
onSidebarOpenClick={() => setIsSidebarOpen(true)}
|
onSidebarOpenClick={() => setIsSidebarOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Menu, X, Terminal as TerminalIcon} from "lucide-react";
|
import {Menu, X, Terminal as TerminalIcon} from "lucide-react";
|
||||||
import {useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx";
|
import {useTabs} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
|
||||||
import {cn} from "@/lib/utils.ts";
|
import {cn} from "@/lib/utils.ts";
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
@@ -11,8 +11,8 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
|||||||
const {tabs, currentTab, setCurrentTab, removeTab} = useTabs();
|
const {tabs, currentTab, setCurrentTab, removeTab} = useTabs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-[60px] bg-[#18181B] items-center p-2">
|
<div className="w-full h-[50px] bg-[#18181B] items-center p-1">
|
||||||
<div className="flex gap-2 mb-1">
|
<div className="flex gap-2 !mb-0.5">
|
||||||
<Button className="w-[40px] h-[40px] flex-shrink-0" variant="outline" onClick={onSidebarOpenClick}>
|
<Button className="w-[40px] h-[40px] flex-shrink-0" variant="outline" onClick={onSidebarOpenClick}>
|
||||||
<Menu/>
|
<Menu/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -3,7 +3,7 @@ import {CardTitle} from "@/components/ui/card.tsx";
|
|||||||
import {ChevronDown, Folder} from "lucide-react";
|
import {ChevronDown, Folder} from "lucide-react";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {Host} from "@/ui/Mobile/Apps/Navigation/Hosts/Host.tsx";
|
import {Host} from "@/ui/Mobile/Navigation/Hosts/Host.tsx";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -4,7 +4,7 @@ import {Button} from "@/components/ui/button.tsx";
|
|||||||
import {ButtonGroup} from "@/components/ui/button-group.tsx";
|
import {ButtonGroup} from "@/components/ui/button-group.tsx";
|
||||||
import {Server, Terminal} from "lucide-react";
|
import {Server, Terminal} from "lucide-react";
|
||||||
import {getServerStatusById} from "@/ui/main-axios.ts";
|
import {getServerStatusById} from "@/ui/main-axios.ts";
|
||||||
import {useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx";
|
import {useTabs} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -10,7 +10,7 @@ import {Button} from "@/components/ui/button.tsx";
|
|||||||
import {ChevronUp, Menu, User2} from "lucide-react";
|
import {ChevronUp, Menu, User2} from "lucide-react";
|
||||||
import React, {useState, useEffect, useMemo, useCallback} from "react";
|
import React, {useState, useEffect, useMemo, useCallback} from "react";
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {FolderCard} from "@/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx";
|
import {FolderCard} from "@/ui/Mobile/Navigation/Hosts/FolderCard.tsx";
|
||||||
import {getSSHHosts} from "@/ui/main-axios.ts";
|
import {getSSHHosts} from "@/ui/main-axios.ts";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {Input} from "@/components/ui/input.tsx";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
Reference in New Issue
Block a user