Added initial split screen mode and overall better UI sizing for terminals. (very buggy)
This commit is contained in:
103
src/App.jsx
103
src/App.jsx
@@ -23,7 +23,7 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
const [isLaunchpadOpen, setIsLaunchpadOpen] = useState(false);
|
||||||
|
|
||||||
// Toggle Launchpad when "L" key is pressed
|
// Handle keypress for opening launchpad
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.ctrlKey && e.key === "l") {
|
if (e.ctrlKey && e.key === "l") {
|
||||||
@@ -38,6 +38,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle adding a host
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
if (form.ip && form.user && form.password && form.port) {
|
if (form.ip && form.user && form.password && form.port) {
|
||||||
const newTerminal = {
|
const newTerminal = {
|
||||||
@@ -49,6 +50,8 @@ function App() {
|
|||||||
password: form.password,
|
password: form.password,
|
||||||
port: Number(form.port),
|
port: Number(form.port),
|
||||||
},
|
},
|
||||||
|
isSplit: false,
|
||||||
|
terminalRef: null, // Reference to the terminal instance
|
||||||
};
|
};
|
||||||
setTerminals([...terminals, newTerminal]);
|
setTerminals([...terminals, newTerminal]);
|
||||||
setActiveTab(nextId);
|
setActiveTab(nextId);
|
||||||
@@ -60,6 +63,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close a terminal tab
|
||||||
const closeTab = (id) => {
|
const closeTab = (id) => {
|
||||||
const newTerminals = terminals.filter((t) => t.id !== id);
|
const newTerminals = terminals.filter((t) => t.id !== id);
|
||||||
setTerminals(newTerminals);
|
setTerminals(newTerminals);
|
||||||
@@ -68,39 +72,42 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toggle split for a specific tab
|
||||||
|
const toggleSplit = (id) => {
|
||||||
|
setTerminals(terminals.map(t =>
|
||||||
|
t.id === id ? { ...t, isSplit: !t.isSplit } : t
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the split terminals
|
||||||
|
const splitTerminals = terminals.filter(t => t.isSplit);
|
||||||
|
const mainTerminal = terminals.find(t => t.id === activeTab);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
<div className="flex h-screen bg-neutral-900 overflow-hidden">
|
<div className="flex h-screen bg-neutral-900 overflow-hidden">
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Topbar */}
|
{/* Topbar */}
|
||||||
<div className="bg-neutral-800 text-white p-4 flex items-center justify-between gap-4 min-h-[65px]">
|
<div className="bg-neutral-800 text-white p-4 flex items-center justify-between gap-4 min-h-[75px] max-h-[75px]">
|
||||||
{/* Left: Title */}
|
|
||||||
<div className="bg-neutral-700 flex justify-center items-center gap-2 p-3 rounded-lg h-[52px]">
|
<div className="bg-neutral-700 flex justify-center items-center gap-2 p-3 rounded-lg h-[52px]">
|
||||||
<img
|
<img src={TermixIcon} alt="Termix Icon" className="w-[30px] h-[30px]" />
|
||||||
src={TermixIcon}
|
|
||||||
alt="Termix Icon"
|
|
||||||
className="w-[30px] h-[30px]"
|
|
||||||
/>
|
|
||||||
<h2 className="text-lg font-bold ml-[-2px]">Termix</h2>
|
<h2 className="text-lg font-bold ml-[-2px]">Termix</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle: Tabs with scroll */}
|
|
||||||
<div className="flex-1 bg-neutral-700 rounded-lg overflow-hidden h-[52px] flex items-center">
|
<div className="flex-1 bg-neutral-700 rounded-lg overflow-hidden h-[52px] flex items-center">
|
||||||
<div
|
<div className="flex-1 overflow-x-auto overflow-y-hidden scrollbar-thin scrollbar-thumb-neutral-500 scrollbar-track-neutral-700 h-[52px] scrollbar-thumb-rounded-full scrollbar-track-rounded-full scrollbar-h-1">
|
||||||
className="flex-1 overflow-x-auto overflow-y-hidden scrollbar-thin scrollbar-thumb-neutral-500 scrollbar-track-neutral-700 h-[52px] scrollbar-thumb-rounded-full scrollbar-track-rounded-full scrollbar-h-1"
|
|
||||||
style={{ whiteSpace: "nowrap" }}
|
|
||||||
>
|
|
||||||
<TabList
|
<TabList
|
||||||
terminals={terminals}
|
terminals={terminals}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
closeTab={closeTab}
|
closeTab={closeTab}
|
||||||
|
toggleSplit={toggleSplit}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Open Launchpad Button */}
|
{/* Launchpad Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsLaunchpadOpen(true)}
|
onClick={() => setIsLaunchpadOpen(true)}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -109,21 +116,13 @@ function App() {
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
height: "52px",
|
height: "52px",
|
||||||
width: "52px",
|
width: "52px",
|
||||||
padding: 0, // To ensure no padding around the image
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img src={RocketIcon} alt="Launchpad" style={{ width: "70%", height: "70", objectFit: "contain" }} />
|
||||||
src={RocketIcon}
|
|
||||||
alt="L"
|
|
||||||
style={{
|
|
||||||
width: "70%",
|
|
||||||
height: "70",
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Right: Create Host Button */}
|
{/* Add Host Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsAddHostHidden(false)}
|
onClick={() => setIsAddHostHidden(false)}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -136,9 +135,7 @@ function App() {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
lineHeight: "normal",
|
|
||||||
paddingTop: "2px",
|
paddingTop: "2px",
|
||||||
verticalAlign: "middle",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
@@ -146,20 +143,48 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Terminal Views */}
|
{/* Terminal Views */}
|
||||||
<div className="flex-1 relative pt-12 overflow-hidden">
|
<div className="flex-1 relative p-4">
|
||||||
{terminals.map((terminal) => (
|
{splitTerminals.length > 0 ? (
|
||||||
<div
|
<div className={`grid ${splitTerminals.length === 1 ? 'grid-cols-1' : 'grid-cols-2'} gap-4 h-full`}>
|
||||||
key={terminal.id}
|
{splitTerminals.map((terminal) => (
|
||||||
className={`absolute top-0 left-0 right-0 bottom-0 ${
|
<div key={terminal.id} className="bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 h-full">
|
||||||
terminal.id === activeTab ? "block" : "hidden"
|
<NewTerminal
|
||||||
}`}
|
hostConfig={terminal.hostConfig}
|
||||||
>
|
ref={(ref) => {
|
||||||
<NewTerminal hostConfig={terminal.hostConfig} />
|
if (ref && !terminal.terminalRef) {
|
||||||
|
// Store the terminal instance reference
|
||||||
|
setTerminals(prev => prev.map(t =>
|
||||||
|
t.id === terminal.id ? { ...t, terminalRef: ref } : t
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
<div className="absolute top-4 left-4 right-4 bottom-4">
|
||||||
|
{mainTerminal && (
|
||||||
|
<div className="bg-neutral-800 rounded-lg overflow-hidden shadow-xl border-5 border-neutral-700 h-full">
|
||||||
|
<NewTerminal
|
||||||
|
hostConfig={mainTerminal.hostConfig}
|
||||||
|
ref={(ref) => {
|
||||||
|
if (ref && !mainTerminal.terminalRef) {
|
||||||
|
// Store the terminal instance reference
|
||||||
|
setTerminals(prev => prev.map(t =>
|
||||||
|
t.id === mainTerminal.id ? { ...t, terminalRef: ref } : t
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal for adding a host */}
|
||||||
<AddHostModal
|
<AddHostModal
|
||||||
isHidden={isAddHostHidden}
|
isHidden={isAddHostHidden}
|
||||||
form={form}
|
form={form}
|
||||||
@@ -169,9 +194,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Launchpad Component */}
|
{/* Launchpad Component */}
|
||||||
{isLaunchpadOpen && (
|
{isLaunchpadOpen && <Launchpad onClose={() => setIsLaunchpadOpen(false)} />}
|
||||||
<Launchpad onClose={() => setIsLaunchpadOpen(false)} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import {Button, ButtonGroup} from "@mui/joy";
|
import { Button, ButtonGroup } from "@mui/joy";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
function TabList({ terminals, activeTab, setActiveTab, closeTab, theme }) {
|
function TabList({ terminals, activeTab, setActiveTab, closeTab, toggleSplit, theme }) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center h-full px-[0.5rem]">
|
<div className="inline-flex items-center h-full px-[0.5rem]">
|
||||||
{terminals.map((terminal, index) => (
|
{terminals.map((terminal, index) => (
|
||||||
<div
|
<div key={terminal.id} className={index < terminals.length - 1 ? "mr-[0.5rem]" : ""}>
|
||||||
key={terminal.id}
|
|
||||||
className={index < terminals.length - 1 ? "mr-[0.5rem]" : ""}
|
|
||||||
>
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setActiveTab(terminal.id)}
|
onClick={() => setActiveTab(terminal.id)}
|
||||||
@@ -29,6 +26,23 @@ function TabList({ terminals, activeTab, setActiveTab, closeTab, theme }) {
|
|||||||
>
|
>
|
||||||
{terminal.title}
|
{terminal.title}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleSplit(terminal.id)}
|
||||||
|
disabled={terminal.id === activeTab}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.neutral[700],
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
"&:hover": { backgroundColor: theme.palette.neutral[300] },
|
||||||
|
borderTopRightRadius: "4px",
|
||||||
|
borderBottomRightRadius: "4px",
|
||||||
|
height: "40px",
|
||||||
|
fontSize: "1rem",
|
||||||
|
opacity: terminal.id === activeTab ? 0.5 : 1,
|
||||||
|
cursor: terminal.id === activeTab ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
/
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => closeTab(terminal.id)}
|
onClick={() => closeTab(terminal.id)}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -38,6 +52,7 @@ function TabList({ terminals, activeTab, setActiveTab, closeTab, theme }) {
|
|||||||
borderTopRightRadius: "4px",
|
borderTopRightRadius: "4px",
|
||||||
borderBottomRightRadius: "4px",
|
borderBottomRightRadius: "4px",
|
||||||
height: "40px",
|
height: "40px",
|
||||||
|
fontSize: "1rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -54,6 +69,7 @@ TabList.propTypes = {
|
|||||||
activeTab: PropTypes.any,
|
activeTab: PropTypes.any,
|
||||||
setActiveTab: PropTypes.func.isRequired,
|
setActiveTab: PropTypes.func.isRequired,
|
||||||
closeTab: PropTypes.func.isRequired,
|
closeTab: PropTypes.func.isRequired,
|
||||||
|
toggleSplit: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.object.isRequired,
|
theme: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function NewTerminal({ hostConfig }) {
|
|||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
theme: {
|
theme: {
|
||||||
background: "#0f0f0f",
|
background: "#242424",
|
||||||
foreground: "#ffffff",
|
foreground: "#ffffff",
|
||||||
cursor: "#ffffff",
|
cursor: "#ffffff",
|
||||||
},
|
},
|
||||||
@@ -36,19 +36,21 @@ export function NewTerminal({ hostConfig }) {
|
|||||||
// Resize function
|
// Resize function
|
||||||
const resizeTerminal = () => {
|
const resizeTerminal = () => {
|
||||||
const terminalContainer = terminalRef.current;
|
const terminalContainer = terminalRef.current;
|
||||||
const topbarHeight = 65;
|
const parentContainer = terminalContainer?.parentElement;
|
||||||
const availableWidth = window.innerWidth;
|
|
||||||
const availableHeight = window.innerHeight - topbarHeight;
|
|
||||||
|
|
||||||
terminalContainer.style.width = `${availableWidth}px`;
|
if (!parentContainer) return;
|
||||||
terminalContainer.style.height = `${availableHeight}px`;
|
|
||||||
|
const parentWidth = parentContainer.clientWidth;
|
||||||
|
const parentHeight = parentContainer.clientHeight;
|
||||||
|
|
||||||
|
terminalContainer.style.width = `${parentWidth}px`;
|
||||||
|
terminalContainer.style.height = `${parentHeight}px`;
|
||||||
|
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
const { cols, rows } = terminal;
|
const { cols, rows } = terminal;
|
||||||
|
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
socketRef.current.emit("resize", { cols, rows });
|
socketRef.current.emit("resize", { cols, rows });
|
||||||
console.log(`Terminal resized: cols=${cols}, rows=${rows}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +120,7 @@ export function NewTerminal({ hostConfig }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={terminalRef}
|
ref={terminalRef}
|
||||||
className="w-full h-full min-h-[400px] overflow-hidden text-left"
|
className="w-full h-full overflow-hidden text-left"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user