Spaces:
Paused
Paused
| # 📢 1. ZeroGPU keepalive setup | |
| import spaces | |
| def _hf_zerogpu_keepalive(): | |
| return None | |
| import os | |
| import sys | |
| import time | |
| import subprocess | |
| import html | |
| # 2. Auto-install required packages | |
| for pkg in ["gradio", "psutil"]: | |
| try: | |
| __import__(pkg) | |
| except ImportError: | |
| subprocess.run([sys.executable, "-m", "pip", "install", pkg, "-q"], check=True) | |
| import gradio as gr | |
| import psutil | |
| # Global directory state | |
| current_working_dir = os.path.expanduser("~") | |
| initial_text = "<div id='welcome_msg' style='color:#8b949e; font-size:11px;'>[TERNAL Terminal Is Ready...]</div>" | |
| # ========================================== | |
| # 3. Terminal Execution Engine | |
| # ========================================== | |
| def execute_terminal(cmd, current_history): | |
| global current_working_dir | |
| if not cmd or not cmd.strip(): | |
| return current_history, "" | |
| cmd_clean = cmd.strip() | |
| if cmd_clean == "clear": | |
| return initial_text, "" | |
| # Remove initial text on first command | |
| if current_history and initial_text in current_history: | |
| current_history = current_history.replace(initial_text, "") | |
| if cmd_clean.startswith("cd ") or cmd_clean == "cd": | |
| target = cmd_clean[3:].strip() if len(cmd_clean) > 2 else os.path.expanduser("~") | |
| if target == "~": | |
| target = os.path.expanduser("~") | |
| try: | |
| new_path = os.path.abspath(os.path.join(current_working_dir, target)) | |
| if os.path.isdir(new_path): | |
| current_working_dir = new_path | |
| output = f"<span style='color: #7ee787;'>[Dir: {current_working_dir}]</span>" | |
| else: | |
| output = f"<span style='color: #ff7b72;'>cd: {target}: No such directory</span>" | |
| except Exception as e: | |
| output = f"<span style='color: #ff7b72;'>cd error: {str(e)}</span>" | |
| else: | |
| try: | |
| res = subprocess.run( | |
| cmd_clean, | |
| shell=True, | |
| cwd=current_working_dir, | |
| capture_output=True, | |
| text=True, | |
| timeout=180 | |
| ) | |
| out_html = "" | |
| if res.stdout: | |
| out_html += f"<span style='color: #a5d6ff;'>{html.escape(res.stdout)}</span>" | |
| if res.stderr: | |
| out_html += f"<span style='color: #ff7b72;'>{html.escape(res.stderr)}</span>" | |
| if not res.stdout and not res.stderr: | |
| out_html = "<span style='color: #8b949e;'>[Process finished with exit code 0]</span>" | |
| output = f"<pre style='margin: 0; font-family: inherit; font-size: 11.5px; white-space: pre-wrap; word-break: break-all;'>{out_html}</pre>" | |
| except subprocess.TimeoutExpired: | |
| output = "<span style='color: #ff7b72;'>[Timeout: 180s limit exceeded!]</span>" | |
| except Exception as e: | |
| output = f"<span style='color: #ff7b72;'>[Error: {html.escape(str(e))}]</span>" | |
| user_part = "<span style='color: #7ee787; font-weight: bold;'>user@hf</span>" | |
| path_part = f"<span style='color: #79c0ff;'>:{os.path.basename(current_working_dir) or '/'}</span>" | |
| cmd_part = f"<span style='color: #f2cc60; font-weight: bold;'>$ {html.escape(cmd_clean)}</span>" | |
| prompt_line = f"<div style='margin-top: 6px; padding-top: 3px; font-size: 11.5px;'>{user_part}{path_part}{cmd_part}</div>" | |
| output_block = f"<div style='padding-left: 2px; margin-bottom: 4px;'>{output}</div>" | |
| updated_history = (current_history if current_history else "") + prompt_line + output_block | |
| scroll_js = "<script>var el = document.getElementById('terminal_output'); if(el) el.scrollTop = el.scrollHeight;</script>" | |
| return updated_history + scroll_js, "" | |
| # ========================================== | |
| # 4. Live RAM & CPU Header | |
| # ========================================== | |
| def get_live_header(): | |
| used_gb = 0.0 | |
| try: | |
| if os.path.exists("/sys/fs/cgroup/memory.current"): | |
| with open("/sys/fs/cgroup/memory.current") as f: | |
| used_gb = round(int(f.read().strip()) / (1024**3), 1) | |
| else: | |
| used_gb = round(psutil.virtual_memory().used / (1024**3), 1) | |
| except: | |
| used_gb = round(psutil.virtual_memory().used / (1024**3), 1) | |
| if used_gb < 0.1 or used_gb > 96.0: | |
| used_gb = round(psutil.virtual_memory().used / (1024**3), 1) | |
| cpu_load = int(psutil.cpu_percent()) | |
| ram_svg = """<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#79c0ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>""" | |
| cpu_svg = """<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7ee787" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>""" | |
| return f""" | |
| <div style='display: flex; justify-content: space-between; align-items: center; width: 100%;'> | |
| <div style='display: flex; align-items: center; gap: 5px; white-space: nowrap;'> | |
| {ram_svg} <span style='color: #79c0ff; font-weight: bold;'>RAM:</span> {used_gb}/96GB | |
| </div> | |
| <div style='display: flex; align-items: center; gap: 5px; white-space: nowrap;'> | |
| {cpu_svg} <span style='color: #7ee787; font-weight: bold;'>CPU:</span> 16C ({cpu_load}%) | |
| </div> | |
| </div> | |
| """ | |
| # ========================================== | |
| # 5. Bulletproof CSS (Hardware Engine Auto-Lock) | |
| # ========================================== | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap'); | |
| footer { display: none !important; } | |
| html, body { | |
| height: 100dvh !important; | |
| width: 100vw !important; | |
| background-color: #05070a !important; | |
| overflow: hidden !important; | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| position: fixed !important; | |
| } | |
| .gradio-container { | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| max-width: 100% !important; | |
| width: 100vw !important; | |
| height: 100dvh !important; | |
| border: none !important; | |
| background-color: #05070a !important; | |
| } | |
| #main_wrapper { | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: 0 !important; | |
| width: 100vw !important; | |
| height: 100dvh !important; | |
| background-color: #05070a !important; | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| } | |
| /* Top Header */ | |
| #live_header { | |
| position: fixed !important; | |
| top: 0 !important; | |
| left: 0 !important; | |
| width: 100vw !important; | |
| height: 32px !important; | |
| background-color: #161b22 !important; | |
| border-bottom: 1px solid #30363d !important; | |
| padding: 0 15px !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| box-sizing: border-box !important; | |
| z-index: 9999 !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 11px !important; | |
| } | |
| #live_header > div { width: 100% !important; } | |
| /* TERNAL Sub-header */ | |
| #ternal_branding { | |
| position: fixed !important; | |
| top: 32px !important; | |
| left: 0 !important; | |
| width: 100vw !important; | |
| height: 26px !important; | |
| background-color: #0d1117 !important; | |
| border-bottom: none !important; | |
| display: flex !important; | |
| justify-content: center !important; | |
| align-items: center !important; | |
| box-sizing: border-box !important; | |
| z-index: 9998 !important; | |
| } | |
| .ternal-text { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11.5px; | |
| font-weight: bold; | |
| letter-spacing: 1.5px; | |
| background: linear-gradient(90deg, #ff007f, #79c0ff, #7ee787, #f2cc60, #ff007f); | |
| background-size: 200% auto; | |
| color: transparent !important; | |
| -webkit-background-clip: text !important; | |
| -webkit-text-fill-color: transparent !important; | |
| animation: shine 3s linear infinite; | |
| margin-right: 6px; | |
| } | |
| @keyframes shine { to { background-position: 200% center; } } | |
| /* Output screen */ | |
| #terminal_output { | |
| position: fixed !important; | |
| top: 58px !important; | |
| bottom: 75px !important; | |
| left: 0 !important; | |
| width: 100vw !important; | |
| overflow-y: auto !important; | |
| padding: 10px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| box-sizing: border-box !important; | |
| background-color: #05070a !important; | |
| } | |
| /* Bottom Bar */ | |
| #bottom_row { | |
| position: fixed !important; | |
| bottom: 0 !important; | |
| left: 0 !important; | |
| width: 100vw !important; | |
| height: 75px !important; | |
| background-color: #0b0e14 !important; | |
| border-top: 1px solid #30363d !important; | |
| display: flex !important; | |
| flex-direction: row !important; | |
| align-items: flex-start !important; | |
| padding: 8px 8px 25px 8px !important; | |
| box-sizing: border-box !important; | |
| z-index: 9999 !important; | |
| gap: 8px !important; | |
| } | |
| /* Clear button */ | |
| #btn_clear { | |
| background-color: #da3633 !important; | |
| color: #ffffff !important; | |
| min-width: 75px !important; | |
| max-width: 75px !important; | |
| height: 40px !important; | |
| font-size: 12px !important; | |
| border-radius: 6px !important; | |
| border: none !important; | |
| display: flex !important; | |
| justify-content: center !important; | |
| align-items: center !important; | |
| cursor: pointer !important; | |
| } | |
| #btn_clear::before { | |
| content: ""; | |
| display: inline-block; | |
| width: 14px; | |
| height: 14px; | |
| margin-right: 5px; | |
| background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBvbHlsaW5lIHBvaW50cz0iMyA2IDUgNiAyMSA2Ij48L3BvbHlsaW5lPjxwYXRoIGQ9Ik0xOSA2djE0YTIgMiAwIDAgMS0yIDJIN2EyIDIgMCAwIDEtMi0yVjZtMyAwVjRhMiAyIDAgMCAxIDItMmg0YTIgMiAwIDAgMSAyIDJ2MiI+PC9wYXRoPjwvc3ZnPg=="); | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| } | |
| #cmd_input { | |
| flex-grow: 1 !important; | |
| height: 40px !important; | |
| } | |
| #cmd_input input, #cmd_input textarea { | |
| background-color: #161b22 !important; | |
| color: #58a6ff !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 14px !important; | |
| border: 1px solid #30363d !important; | |
| border-radius: 6px !important; | |
| height: 40px !important; | |
| padding: 0 10px !important; | |
| width: 100% !important; | |
| box-sizing: border-box !important; | |
| } | |
| /* Send button default active state */ | |
| #btn_send { | |
| background-color: #238636 !important; | |
| color: #ffffff !important; | |
| min-width: 75px !important; | |
| max-width: 75px !important; | |
| height: 40px !important; | |
| font-size: 12px !important; | |
| border-radius: 6px !important; | |
| border: none !important; | |
| display: flex !important; | |
| justify-content: center !important; | |
| align-items: center !important; | |
| cursor: pointer !important; | |
| transition: all 0.15s ease !important; | |
| } | |
| #btn_send::before { | |
| content: ""; | |
| display: inline-block; | |
| width: 14px; | |
| height: 14px; | |
| margin-right: 5px; | |
| background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PGxpbmUgeDE9IjIyIiB5MT0iMiIgeDI9IjExIiB5Mj0iMTMiPjwvbGluZT48cG9seWdvbiBwb2ludHM9IjIyIDIgMTUgMjIgMTEgMTMgMiA5IDIyIDIiPjwvcG9seWdvbj48L3N2Zz4="); | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| } | |
| /* 🔒 1. CSS Engine Auto-Lock: ইনপুট খালি থাকলে বাটন ১০০% লক ও ধূসর */ | |
| #bottom_row:has(#cmd_input input:placeholder-shown) #btn_send, | |
| #bottom_row:has(#cmd_input textarea:placeholder-shown) #btn_send { | |
| background-color: #21262d !important; | |
| color: #6e7681 !important; | |
| opacity: 0.35 !important; | |
| pointer-events: none !important; | |
| cursor: not-allowed !important; | |
| } | |
| #bottom_row:has(#cmd_input input:placeholder-shown) #btn_send::before, | |
| #bottom_row:has(#cmd_input textarea:placeholder-shown) #btn_send::before { | |
| opacity: 0.25 !important; | |
| } | |
| /* 🔒 2. Execution Lock: কমান্ড আউটপুট আসার আগ পর্যন্ত লক */ | |
| #btn_send.running-state, | |
| .pending #btn_send { | |
| background-color: #21262d !important; | |
| color: #6e7681 !important; | |
| opacity: 0.35 !important; | |
| pointer-events: none !important; | |
| cursor: not-allowed !important; | |
| } | |
| /* Hidden Action Button */ | |
| #btn_hidden_clear { | |
| position: absolute !important; | |
| top: -9999px !important; | |
| left: -9999px !important; | |
| opacity: 0 !important; | |
| width: 1px !important; | |
| height: 1px !important; | |
| pointer-events: none !important; | |
| } | |
| """ | |
| # HTML for in-app Dark Modal Dialog | |
| modal_html = """ | |
| <div id="clear_modal" style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); backdrop-filter: blur(8px); z-index: 100000; justify-content: center; align-items: center; margin: 0; padding: 0;"> | |
| <div style="background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 22px; width: 85%; max-width: 310px; text-align: center; box-shadow: 0 20px 40px rgba(0,0,0,0.9); font-family: 'JetBrains Mono', monospace;"> | |
| <div style="font-size: 15px; font-weight: bold; color: #f85149; margin-bottom: 8px;">Clear Terminal?</div> | |
| <div style="font-size: 11.5px; color: #8b949e; margin-bottom: 20px; line-height: 1.5;">This will clear all current screen logs. Are you sure?</div> | |
| <div style="display: flex; gap: 10px; justify-content: center;"> | |
| <button onclick="document.getElementById('clear_modal').style.display='none';" style="background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 8px 18px; border-radius: 6px; font-size: 12px; font-weight: bold; cursor: pointer;">Cancel</button> | |
| <button onclick="document.getElementById('clear_modal').style.display='none'; const el = document.getElementById('btn_hidden_clear') || document.querySelector('#btn_hidden_clear button'); if(el) el.click();" style="background: #da3633; color: #ffffff; border: none; padding: 8px 18px; border-radius: 6px; font-size: 12px; font-weight: bold; cursor: pointer;">Confirm</button> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # TERNAL Sub-header Branding | |
| ternal_html = """ | |
| <div id="ternal_branding"> | |
| <span class="ternal-text">TERNAL</span> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#79c0ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="4 17 10 11 4 5"></polyline> | |
| <line x1="12" y1="19" x2="20" y2="19"></line> | |
| </svg> | |
| </div> | |
| """ | |
| # Client-side dynamic execution tracker | |
| client_js = """ | |
| () => { | |
| function initTracker() { | |
| const sendBtn = document.querySelector('#btn_send'); | |
| const term = document.querySelector('#terminal_output'); | |
| const inp = document.querySelector('#cmd_input input') || document.querySelector('#cmd_input textarea'); | |
| if (!sendBtn || !term || !inp) return; | |
| function lockButton() { | |
| sendBtn.classList.add('running-state'); | |
| } | |
| inp.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && inp.value.trim().length > 0) { | |
| lockButton(); | |
| } | |
| }); | |
| sendBtn.addEventListener('click', lockButton); | |
| // Unlock when output arrives on screen | |
| const observer = new MutationObserver(() => { | |
| sendBtn.classList.remove('running-state'); | |
| }); | |
| observer.observe(term, { childList: true, subtree: true, characterData: true }); | |
| } | |
| setTimeout(initTracker, 300); | |
| setTimeout(initTracker, 1000); | |
| } | |
| """ | |
| # ========================================== | |
| # 6. Gradio UI | |
| # ========================================== | |
| with gr.Blocks(title="Ternal", css=custom_css, js=client_js) as demo: | |
| with gr.Column(elem_id="main_wrapper"): | |
| # In-App Custom Modal | |
| modal_component = gr.HTML(value=modal_html) | |
| # Top Live Header (RAM & CPU) | |
| header_bar = gr.HTML(value=get_live_header, every=1.5, elem_id="live_header") | |
| # TERNAL Branding | |
| ternal_bar = gr.HTML(value=ternal_html) | |
| # Terminal Output Screen | |
| terminal_box = gr.HTML(value=initial_text, elem_id="terminal_output") | |
| # Bottom Controls | |
| with gr.Row(elem_id="bottom_row"): | |
| btn_clear = gr.Button("Clear", elem_id="btn_clear") | |
| cmd_input = gr.Textbox( | |
| label="", | |
| placeholder="Enter command (e.g. ls, free -h)...", | |
| elem_id="cmd_input", | |
| container=False, | |
| max_lines=1 | |
| ) | |
| btn_send = gr.Button("Send", elem_id="btn_send") | |
| # Hidden button for modal trigger | |
| btn_hidden_clear = gr.Button("HiddenClear", elem_id="btn_hidden_clear") | |
| # 1. Clear button opens modal | |
| btn_clear.click(fn=None, js="() => { document.getElementById('clear_modal').style.display = 'flex'; }") | |
| # 2. Modal's confirm button triggers clear action | |
| btn_hidden_clear.click(lambda: (initial_text, ""), outputs=[terminal_box, cmd_input]) | |
| # 3. Command execution | |
| cmd_input.submit(execute_terminal, inputs=[cmd_input, terminal_box], outputs=[terminal_box, cmd_input]) | |
| btn_send.click(execute_terminal, inputs=[cmd_input, terminal_box], outputs=[terminal_box, cmd_input]) | |
| if __name__ == "__main__": | |
| demo.launch() |