Spaces:
Build error
Build error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"/> | |
| <title>HF Terminal</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet"/> | |
| <style> | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg0: #0b0f1a; | |
| --bg1: #0f1625; | |
| --bg2: #131d30; | |
| --bg3: #172240; | |
| --bg-input: #0d1520; | |
| --accent: #3b82f6; | |
| --accent2: #60a5fa; | |
| --cyan: #22d3ee; | |
| --green: #34d399; | |
| --yellow: #fbbf24; | |
| --red: #f87171; | |
| --purple: #a78bfa; | |
| --text: #cbd5e1; | |
| --text-dim: #64748b; | |
| --text-mute: #94a3b8; | |
| --border: #1e2d4a; | |
| --titlebar: #080c14; | |
| --font: 'JetBrains Mono','Cascadia Code','Consolas',monospace; | |
| --fz: 13px; | |
| --lh: 1.6; | |
| } | |
| html,body{ | |
| height:100%; | |
| background:#04060e; | |
| display:flex;align-items:center;justify-content:center; | |
| font-family:var(--font);overflow:hidden; | |
| } | |
| .window{ | |
| width:min(980px,98vw);height:min(700px,97vh); | |
| display:flex;flex-direction:column; | |
| border-radius:8px;overflow:hidden; | |
| border:1px solid #1a2744; | |
| box-shadow:0 0 0 1px #06090f,0 24px 80px rgba(0,0,0,.8),0 0 120px rgba(59,130,246,.06); | |
| animation:open .2s cubic-bezier(.22,1,.36,1); | |
| } | |
| @keyframes open{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:none}} | |
| /* TITLEBAR */ | |
| .titlebar{ | |
| flex-shrink:0;height:38px;background:var(--titlebar); | |
| display:flex;align-items:center;padding:0 14px;gap:12px; | |
| border-bottom:1px solid #0e1420;user-select:none; | |
| } | |
| .tb-dots{display:flex;gap:7px;align-items:center} | |
| .dot{width:12px;height:12px;border-radius:50%;cursor:pointer;transition:filter .15s} | |
| .dot.r{background:#ff5f57}.dot.r:hover{filter:brightness(1.2)} | |
| .dot.y{background:#febc2e}.dot.y:hover{filter:brightness(1.2)} | |
| .dot.g{background:#28c840}.dot.g:hover{filter:brightness(1.2)} | |
| .tb-title{flex:1;text-align:center;font-size:12px;color:#3a4a60;letter-spacing:.5px} | |
| .tb-brand{display:flex;align-items:center;gap:7px} | |
| .tb-icon{ | |
| width:22px;height:22px;border-radius:5px; | |
| background:linear-gradient(135deg,#3b82f6,#1d4ed8); | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:9px;font-weight:700;color:#fff;letter-spacing:-.3px; | |
| } | |
| .tb-name{font-size:11.5px;color:#3d5070;font-weight:500;letter-spacing:1.5px;text-transform:uppercase} | |
| /* TABBAR */ | |
| .tabbar{ | |
| flex-shrink:0;height:34px;background:#0a0e18; | |
| display:flex;align-items:flex-end;padding:0 12px;gap:2px; | |
| border-bottom:1px solid var(--border); | |
| } | |
| .tab{ | |
| height:28px;padding:0 16px;font-size:11.5px;font-family:var(--font); | |
| border-radius:6px 6px 0 0;display:flex;align-items:center;gap:6px; | |
| cursor:pointer;border:1px solid transparent;border-bottom:none; | |
| transition:background .15s;color:var(--text-dim);user-select:none; | |
| } | |
| .tab.active{background:var(--bg1);border-color:var(--border);color:var(--text-mute)} | |
| .tab-dot{width:6px;height:6px;border-radius:50%;background:var(--green)} | |
| .tab-plus{ | |
| margin-left:4px;height:28px;padding:0 10px;font-size:17px; | |
| color:var(--text-dim);display:flex;align-items:center;cursor:pointer; | |
| border-radius:4px;transition:background .12s; | |
| } | |
| .tab-plus:hover{background:rgba(255,255,255,.04);color:var(--text-mute)} | |
| /* BODY */ | |
| .body{flex:1;background:var(--bg1);display:flex;flex-direction:column;overflow:hidden;position:relative} | |
| /* OUTPUT */ | |
| #output{ | |
| flex:1;overflow-y:auto;overflow-x:hidden; | |
| padding:14px 18px 8px; | |
| font-size:var(--fz);line-height:var(--lh); | |
| color:var(--text);white-space:pre-wrap;word-break:break-word; | |
| } | |
| #output::-webkit-scrollbar{width:6px} | |
| #output::-webkit-scrollbar-track{background:transparent} | |
| #output::-webkit-scrollbar-thumb{background:#1a2a4a;border-radius:3px} | |
| #output::-webkit-scrollbar-thumb:hover{background:#243858} | |
| .ln{display:block;min-height:1.6em} | |
| .ln-prompt{color:var(--text)} | |
| .p-user{color:var(--cyan);font-weight:500} | |
| .p-at{color:var(--text-dim)} | |
| .p-host{color:var(--accent2)} | |
| .p-colon{color:var(--text-dim)} | |
| .p-path{color:var(--green)} | |
| .p-dollar{color:var(--yellow)} | |
| .p-cmd{color:var(--text)} | |
| .ln-out{color:var(--text)} | |
| .ln-err{color:var(--red)} | |
| .ln-info{color:var(--accent2)} | |
| .ln-ok{color:var(--green)} | |
| .ln-dim{color:var(--text-dim)} | |
| .ln-empty{min-height:.7em} | |
| .banner{border-left:2px solid var(--accent);padding:6px 0 6px 14px;margin-bottom:6px;line-height:1.9} | |
| .banner-title{font-size:15px;font-weight:600;color:var(--accent2);letter-spacing:.5px} | |
| .banner-sub{font-size:12px;color:var(--text-dim)} | |
| .badge{ | |
| display:inline-block;padding:1px 8px;border-radius:3px; | |
| font-size:11px;font-weight:500; | |
| background:rgba(59,130,246,.15);color:var(--accent2); | |
| border:1px solid rgba(59,130,246,.25);margin-left:6px; | |
| } | |
| .tip-key{ | |
| display:inline-block;padding:0 5px;border-radius:3px; | |
| font-size:11px;background:rgba(255,255,255,.06); | |
| color:var(--yellow);border:1px solid rgba(255,255,255,.1); | |
| } | |
| /* INPUT ROW */ | |
| .input-row{ | |
| flex-shrink:0;background:var(--bg-input); | |
| border-top:1px solid var(--border); | |
| padding:6px 18px 7px; | |
| display:flex;align-items:center;position:relative;z-index:2; | |
| } | |
| .prompt-static{font-size:var(--fz);white-space:nowrap;flex-shrink:0;display:flex;align-items:center} | |
| .pi-user{color:var(--cyan);font-weight:500} | |
| .pi-at{color:var(--text-dim)} | |
| .pi-host{color:var(--accent2)} | |
| .pi-colon{color:var(--text-dim)} | |
| .pi-path{color:var(--green)} | |
| .pi-dollar{color:var(--yellow);margin-right:8px} | |
| .cursor-wrap{flex:1;display:inline-flex;align-items:center;position:relative;overflow:hidden} | |
| #cmd{ | |
| flex:1;background:transparent;border:none;outline:none; | |
| font-family:var(--font);font-size:var(--fz); | |
| color:#e2e8f0;caret-color:transparent;padding:0;line-height:var(--lh); | |
| } | |
| .caret{ | |
| position:absolute;width:7px;height:14px; | |
| background:rgba(203,213,225,.8);pointer-events:none; | |
| top:50%;transform:translateY(-50%); | |
| animation:blink 1.05s step-end infinite; | |
| } | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} | |
| /* STATUSBAR */ | |
| .statusbar{ | |
| flex-shrink:0;height:24px;background:#090d16; | |
| border-top:1px solid #0d1422; | |
| display:flex;align-items:stretch; | |
| font-size:11px;color:var(--text-dim); | |
| } | |
| .sb-seg{ | |
| display:flex;align-items:center;gap:5px;padding:0 12px; | |
| border-right:1px solid #131d2e; | |
| } | |
| .sb-seg:last-child{border-right:none;margin-left:auto} | |
| .sb-led{width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0} | |
| .sb-led.busy{background:var(--yellow);animation:pulse .6s ease infinite alternate} | |
| @keyframes pulse{to{opacity:.3}} | |
| .sb-branch{color:var(--purple)} | |
| .spin{display:none;width:9px;height:9px;border:1.5px solid rgba(255,255,255,.1);border-top-color:var(--accent2);border-radius:50%;animation:sp .5s linear infinite;margin-left:6px} | |
| .spin.on{display:inline-block} | |
| @keyframes sp{to{transform:rotate(360deg)}} | |
| ::selection{background:#1e3a5f;color:#e2e8f0} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="window" id="win"> | |
| <div class="titlebar"> | |
| <div class="tb-dots"> | |
| <div class="dot r"></div> | |
| <div class="dot y" onclick="toggleMax()"></div> | |
| <div class="dot g" onclick="toggleMax()"></div> | |
| </div> | |
| <div class="tb-title" id="tbTitle">user@hf-space: ~ β bash</div> | |
| <div class="tb-brand"> | |
| <div class="tb-icon">HF</div> | |
| <span class="tb-name">Terminal</span> | |
| </div> | |
| </div> | |
| <div class="tabbar"> | |
| <div class="tab active"><div class="tab-dot"></div>bash</div> | |
| <div class="tab-plus">+</div> | |
| </div> | |
| <div class="body"> | |
| <div id="output"></div> | |
| <div class="input-row"> | |
| <span class="prompt-static"> | |
| <span class="pi-user">user</span><span class="pi-at">@</span><span class="pi-host">hf-space</span><span class="pi-colon">:</span><span class="pi-path" id="iPath">~</span><span class="pi-dollar"> $ </span> | |
| </span> | |
| <div class="cursor-wrap"> | |
| <input id="cmd" type="text" autocomplete="off" autocorrect="off" spellcheck="false" autofocus/> | |
| <div class="caret" id="caret"></div> | |
| </div> | |
| <span class="spin" id="spin"></span> | |
| </div> | |
| </div> | |
| <div class="statusbar"> | |
| <div class="sb-seg"> | |
| <div class="sb-led" id="led"></div> | |
| <span id="sbStatus">ready</span> | |
| </div> | |
| <div class="sb-seg"><span class="sb-branch">β main</span></div> | |
| <div class="sb-seg"><span id="sbCwd">/app</span></div> | |
| <div class="sb-seg" style="margin-left:auto;border-left:1px solid #131d2e;border-right:none"><span id="sbTime"></span></div> | |
| </div> | |
| </div> | |
| <script> | |
| let cwd='/app',hist=[],hidx=-1,busy=false; | |
| const out=document.getElementById('output'); | |
| const inp=document.getElementById('cmd'); | |
| const iPath=document.getElementById('iPath'); | |
| const led=document.getElementById('led'); | |
| const sbSt=document.getElementById('sbStatus'); | |
| const sbCwd=document.getElementById('sbCwd'); | |
| const spin=document.getElementById('spin'); | |
| const caret=document.getElementById('caret'); | |
| const tbTitle=document.getElementById('tbTitle'); | |
| setInterval(()=>{document.getElementById('sbTime').textContent=new Date().toLocaleTimeString('en-GB')},1000); | |
| document.getElementById('sbTime').textContent=new Date().toLocaleTimeString('en-GB'); | |
| function syncCaret(){ | |
| const tmp=document.createElement('span'); | |
| tmp.style.cssText='font:'+getComputedStyle(inp).font+';visibility:hidden;white-space:pre;position:absolute;top:-9999px'; | |
| tmp.textContent=inp.value.substring(0,inp.selectionStart); | |
| document.body.appendChild(tmp); | |
| caret.style.left=tmp.getBoundingClientRect().width+'px'; | |
| document.body.removeChild(tmp); | |
| } | |
| inp.addEventListener('input',syncCaret); | |
| inp.addEventListener('keyup',syncCaret); | |
| inp.addEventListener('click',syncCaret); | |
| inp.addEventListener('focus',()=>{caret.style.display='block';syncCaret()}); | |
| inp.addEventListener('blur',()=>{caret.style.display='none'}); | |
| document.addEventListener('click',()=>inp.focus()); | |
| function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')} | |
| function pathDisplay(p){ | |
| return p.replace(/^\/app/,'~').replace(/^\/home\/user/,'~')||'~'; | |
| } | |
| function appendLine(text,cls='ln-out'){ | |
| text.split('\n').forEach(l=>{ | |
| const el=document.createElement('span'); | |
| el.className='ln '+cls; | |
| el.textContent=l; | |
| out.appendChild(el); | |
| }); | |
| out.scrollTop=out.scrollHeight; | |
| } | |
| function appendPrompt(cmd){ | |
| const el=document.createElement('span'); | |
| el.className='ln ln-prompt'; | |
| const disp=pathDisplay(cwd); | |
| el.innerHTML= | |
| `<span class="p-user">user</span><span class="p-at">@</span><span class="p-host">hf-space</span>`+ | |
| `<span class="p-colon">:</span><span class="p-path">${esc(disp)}</span>`+ | |
| `<span class="p-dollar"> $ </span><span class="p-cmd">${esc(cmd)}</span>`; | |
| out.appendChild(el); | |
| } | |
| function appendEmpty(){const e=document.createElement('span');e.className='ln ln-empty';out.appendChild(e)} | |
| function updatePrompt(){ | |
| const d=pathDisplay(cwd); | |
| iPath.textContent=d; | |
| sbCwd.textContent=cwd; | |
| tbTitle.textContent=`user@hf-space: ${d} β bash`; | |
| } | |
| function setBusy(b){ | |
| busy=b; | |
| led.classList.toggle('busy',b); | |
| sbSt.textContent=b?'runningβ¦':'ready'; | |
| spin.classList.toggle('on',b); | |
| } | |
| /* ββ Banner ββ */ | |
| (function(){ | |
| const b=document.createElement('div'); | |
| b.className='banner'; | |
| b.innerHTML= | |
| `<div class="banner-title">HF Terminal <span class="badge">v1.0</span></div>`+ | |
| `<div class="banner-sub">Hugging Face Docker Space Β· bash 5.2 Β· Python 3.11</div>`+ | |
| `<div class="banner-sub" style="margin-top:5px">`+ | |
| `Press <span class="tip-key">ββ</span> history `+ | |
| `<span class="tip-key">Ctrl+L</span> clear `+ | |
| `type <span class="tip-key" style="color:#fbbf24">help</span> for commands`+ | |
| `</div>`; | |
| out.appendChild(b); | |
| appendEmpty(); | |
| })(); | |
| /* ββ Builtins ββ */ | |
| const BUILTINS={ | |
| clear:()=>{ out.innerHTML='' }, | |
| cls:()=>{ out.innerHTML='' }, | |
| help:()=>{ | |
| appendLine( | |
| ` COMMAND DESCRIPTION | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ls / ll / dir list directory contents | |
| cd <path> change directory | |
| pwd current working directory | |
| cat <file> display file contents | |
| mkdir <dir> create directory | |
| touch <file> create empty file | |
| rm <file> remove file | |
| cp <src> <dst> copy | |
| mv <src> <dst> move / rename | |
| echo <text> print text | |
| env environment variables | |
| whoami current user | |
| uname -a system information | |
| uptime system uptime | |
| ps aux running processes | |
| df -h disk usage | |
| free -h memory usage | |
| python3 -c "β¦" run inline Python | |
| pip list installed packages | |
| curl <url> HTTP request | |
| wget <url> download file | |
| git --version git version | |
| tree directory tree | |
| jq JSON processor | |
| clear / cls clear terminal | |
| help this help message`,'ln-info'); | |
| }, | |
| }; | |
| /* ββ Run ββ */ | |
| async function run(cmd){ | |
| if(busy)return; | |
| cmd=cmd.trim(); | |
| inp.value='';syncCaret(); | |
| if(!cmd){appendEmpty();return;} | |
| hist.unshift(cmd);if(hist.length>500)hist.pop();hidx=-1; | |
| appendPrompt(cmd); | |
| if(BUILTINS[cmd.toLowerCase()]){BUILTINS[cmd.toLowerCase()]();appendEmpty();return;} | |
| busy=true;setBusy(true); | |
| try{ | |
| const res=await fetch('/execute',{ | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({command:cmd,cwd}) | |
| }); | |
| const d=await res.json(); | |
| if(d.output)appendLine(d.output,d.error?'ln-err':'ln-out'); | |
| if(d.cwd&&d.cwd!==cwd){cwd=d.cwd;updatePrompt();} | |
| }catch(e){ | |
| appendLine('error: '+e.message,'ln-err'); | |
| } | |
| appendEmpty();busy=false;setBusy(false); | |
| out.scrollTop=out.scrollHeight; | |
| } | |
| /* ββ Keyboard ββ */ | |
| inp.addEventListener('keydown',e=>{ | |
| if(e.key==='Enter'){e.preventDefault();run(inp.value);return} | |
| if(e.key==='ArrowUp'){ | |
| e.preventDefault(); | |
| if(hidx<hist.length-1){hidx++;inp.value=hist[hidx];setTimeout(()=>{inp.selectionStart=inp.selectionEnd=inp.value.length;syncCaret()},0)} | |
| return; | |
| } | |
| if(e.key==='ArrowDown'){ | |
| e.preventDefault(); | |
| if(hidx>0){hidx--;inp.value=hist[hidx];}else{hidx=-1;inp.value='';} | |
| setTimeout(()=>{inp.selectionStart=inp.selectionEnd=inp.value.length;syncCaret()},0); | |
| return; | |
| } | |
| if(e.key==='l'&&e.ctrlKey){e.preventDefault();out.innerHTML='';return} | |
| if(e.key==='Tab'){e.preventDefault()} | |
| }); | |
| let maxed=false; | |
| function toggleMax(){ | |
| maxed=!maxed; | |
| document.getElementById('win').style.cssText=maxed?'width:100vw;height:100vh;border-radius:0;':''; | |
| } | |
| inp.focus(); | |
| </script> | |
| </body> | |
| </html> |