Terminal / static /index.html
Rafs-an09002's picture
Update static/index.html
33f93dd verified
<!DOCTYPE html>
<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">&nbsp;$&nbsp;</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">βŽ‡&nbsp;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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
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 &nbsp;Β·&nbsp; bash 5.2 &nbsp;Β·&nbsp; Python 3.11</div>`+
`<div class="banner-sub" style="margin-top:5px">`+
`Press <span class="tip-key">↑↓</span> history &nbsp;`+
`<span class="tip-key">Ctrl+L</span> clear &nbsp;`+
`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>