VPS_BY_ALPHA / static /index.html
pabla1322's picture
Update static/index.html
4ec8c55 verified
Raw
History Blame Contribute Delete
8.44 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Ubuntu 24.04 Web Terminal</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<style>
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #090c10;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 24px 16px;
font-family: 'JetBrains Mono', monospace;
}
h1 {
color: #27c93f;
font-size: 15px;
letter-spacing: 2px;
margin-bottom: 16px;
opacity: 0.7;
}
#term-wrap {
width: 100%;
max-width: 960px;
background: #0d1117;
border-radius: 12px;
overflow: hidden;
border: 1px solid #30363d;
display: flex;
flex-direction: column;
box-shadow: 0 8px 40px rgba(0,0,0,0.6);
}
#titlebar {
background: #1c2128;
padding: 11px 16px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #30363d;
user-select: none;
}
.dot {
width: 13px;
height: 13px;
border-radius: 50%;
}
.dot.r { background: #ff5f56; }
.dot.y { background: #ffbd2e; }
.dot.g { background: #27c93f; }
#title-text {
color: #768390;
font-size: 12px;
margin-left: 10px;
}
#copy-btn {
margin-left: auto;
background: #27c93f22;
border: 1px solid #27c93f55;
border-radius: 6px;
color: #27c93f;
font-size: 11px;
padding: 4px 12px;
cursor: pointer;
}
#copy-btn:hover {
background: #27c93f44;
}
#status-bar {
background: #161b22;
padding: 6px 16px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #21262d;
}
#status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff5f56;
}
#status-dot.connected {
background: #27c93f;
}
#status-text {
color: #768390;
font-size: 11px;
}
#reconnect-btn {
margin-left: auto;
background: #27c93f22;
border: 1px solid #27c93f55;
border-radius: 6px;
color: #27c93f;
font-size: 11px;
padding: 4px 10px;
cursor: pointer;
display: none;
}
#reconnect-btn.visible {
display: block;
}
#terminal-container {
padding: 8px;
min-height: 500px;
background: #0d1117;
}
.xterm {
padding: 8px;
}
#ctx-menu {
display: none;
position: fixed;
background: #1c2128;
border: 1px solid #30363d;
border-radius: 8px;
padding: 4px;
z-index: 9999;
min-width: 140px;
}
#ctx-menu button {
display: block;
width: 100%;
background: none;
border: none;
color: #e6edf3;
padding: 8px 12px;
text-align: left;
cursor: pointer;
border-radius: 4px;
font-family: inherit;
}
#ctx-menu button:hover {
background: #27c93f22;
color: #27c93f;
}
#footer {
margin-top: 14px;
color: #444c56;
font-size: 11px;
text-align: center;
line-height: 1.8;
}
</style>
</head>
<body>
<h1>⬛ Ubuntu 24.04 LTS — Web Terminal</h1>
<div id="term-wrap">
<div id="titlebar">
<div class="dot r"></div>
<div class="dot y"></div>
<div class="dot g"></div>
<span id="title-text">ubuntu@space: ~</span>
<button id="copy-btn">
⎘ Copy Selection
</button>
</div>
<div id="status-bar">
<div id="status-dot"></div>
<span id="status-text">
Connecting...
</span>
<button id="reconnect-btn">
↺ Reconnect
</button>
</div>
<div id="terminal-container"></div>
</div>
<div id="ctx-menu">
<button id="ctx-copy">⎘ Copy</button>
<button id="ctx-paste">⎙ Paste</button>
<button id="ctx-clear">✕ Clear</button>
</div>
<div id="footer">
Real Ubuntu shell running on Hugging Face Space
</div>
<script>
let ws = null;
let reconnectTimeout = null;
const term = new Terminal({
cursorBlink: true,
fontSize: 13,
fontFamily: "'JetBrains Mono', monospace",
allowTransparency: false,
scrollback: 3000,
disableStdin: false,
macOptionIsMeta: true,
theme: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#27c93f'
}
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal-container'));
fitAddon.fit();
term.focus();
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const reconnectBtn = document.getElementById('reconnect-btn');
function setStatus(connected, text) {
statusDot.className = connected ? 'connected' : '';
statusText.textContent = text;
reconnectBtn.className = connected ? '' : 'visible';
}
function getWsUrl() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
return `${protocol}://${location.host}/ws`;
}
function connect() {
if (ws) {
ws.close();
ws = null;
}
setStatus(false, 'Connecting...');
ws = new WebSocket(getWsUrl());
ws.onopen = () => {
setStatus(true, 'Connected');
sendResize();
term.focus();
};
ws.onmessage = (event) => {
term.write(event.data);
};
ws.onerror = () => {
setStatus(false, 'Connection error');
};
ws.onclose = () => {
setStatus(false, 'Disconnected');
clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(() => {
connect();
}, 2000);
};
}
function sendResize() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return;
}
ws.send(JSON.stringify({
type: 'resize',
cols: term.cols,
rows: term.rows
}));
}
term.onData((data) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
return;
}
if (typeof data !== 'string') {
return;
}
ws.send(JSON.stringify({
type: 'input',
data: data
}));
});
term.onResize(() => {
sendResize();
});
window.addEventListener('resize', () => {
fitAddon.fit();
sendResize();
});
reconnectBtn.addEventListener('click', connect);
document.getElementById('copy-btn').addEventListener('click', async () => {
const selection = term.getSelection();
if (!selection) return;
await navigator.clipboard.writeText(selection);
const btn = document.getElementById('copy-btn');
btn.textContent = '✓ Copied';
setTimeout(() => {
btn.textContent = '⎘ Copy Selection';
}, 1500);
});
const ctxMenu = document.getElementById('ctx-menu');
document.getElementById('terminal-container')
.addEventListener('contextmenu', (e) => {
e.preventDefault();
ctxMenu.style.display = 'block';
ctxMenu.style.left = `${e.pageX}px`;
ctxMenu.style.top = `${e.pageY}px`;
});
document.addEventListener('click', () => {
ctxMenu.style.display = 'none';
});
document.getElementById('ctx-copy')
.addEventListener('click', async () => {
const selection = term.getSelection();
if (!selection) return;
await navigator.clipboard.writeText(selection);
});
document.getElementById('ctx-paste')
.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
term.paste(text);
} catch (err) {
console.error(err);
}
});
document.getElementById('ctx-clear')
.addEventListener('click', () => {
term.clear();
});
// Prevent duplicate paste bubbling
setTimeout(() => {
if (term.textarea) {
term.textarea.addEventListener('paste', (e) => {
e.stopPropagation();
});
}
}, 500);
connect();
</script>
</body>
</html>