Spaces:
Sleeping
Sleeping
| <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> |