Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -30,52 +30,109 @@ SCREEN_W, SCREEN_H = 800, 600
|
|
| 30 |
SNIPPETS_FILE = "/tmp/axon_snippets.json"
|
| 31 |
|
| 32 |
# ═══════════════════════════════════════
|
| 33 |
-
# Virtual Display
|
| 34 |
# ═══════════════════════════════════════
|
| 35 |
class VirtualDisplay:
|
| 36 |
def __init__(self):
|
| 37 |
self.xvfb_proc = None
|
| 38 |
self.display = DISPLAY_NUM
|
|
|
|
| 39 |
self._start_xvfb()
|
| 40 |
|
| 41 |
def _start_xvfb(self):
|
| 42 |
try:
|
| 43 |
-
subprocess.run(["pkill", "-f", f"Xvfb {self.display}"],
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
if self.xvfb_proc.poll() is None:
|
| 51 |
-
print(f"[Xvfb] Running on {self.display}")
|
| 52 |
os.environ["DISPLAY"] = self.display
|
|
|
|
| 53 |
else:
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
except Exception as e:
|
| 56 |
print(f"[Xvfb] {e}"); self.xvfb_proc = None
|
| 57 |
|
| 58 |
def capture(self):
|
|
|
|
| 59 |
if not self.is_running: return None
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
]:
|
| 65 |
-
try:
|
| 66 |
-
kw = {"shell": True} if isinstance(cmd, str) else {}
|
| 67 |
-
r = subprocess.run(cmd, capture_output=True, timeout=3,
|
| 68 |
-
env={**os.environ, "DISPLAY": self.display}, **kw)
|
| 69 |
-
if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:
|
| 70 |
-
return tmp.name
|
| 71 |
except: pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
try: os.unlink(tmp.name)
|
| 73 |
except: pass
|
| 74 |
return None
|
| 75 |
|
| 76 |
@property
|
| 77 |
-
def is_running(self):
|
|
|
|
|
|
|
| 78 |
def cleanup(self):
|
|
|
|
|
|
|
|
|
|
| 79 |
if self.xvfb_proc:
|
| 80 |
self.xvfb_proc.terminate()
|
| 81 |
try: self.xvfb_proc.wait(timeout=3)
|
|
@@ -84,8 +141,10 @@ class VirtualDisplay:
|
|
| 84 |
vdisplay = VirtualDisplay()
|
| 85 |
|
| 86 |
# ═══════════════════════════════════════
|
| 87 |
-
# GUI Process Manager
|
| 88 |
# ═══════════════════════════════════════
|
|
|
|
|
|
|
| 89 |
class GUIProcessManager:
|
| 90 |
def __init__(self):
|
| 91 |
self.process = None
|
|
@@ -94,28 +153,63 @@ class GUIProcessManager:
|
|
| 94 |
self.stop()
|
| 95 |
tmp = "/tmp/_axon_gui_run.py"
|
| 96 |
with open(tmp, "w") as f: f.write(code)
|
|
|
|
|
|
|
| 97 |
env = os.environ.copy()
|
| 98 |
-
env.update({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
try:
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
if self.process.poll() is not None:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
def stop(self):
|
| 111 |
if self.process and self.process.poll() is None:
|
| 112 |
pid = self.process.pid
|
| 113 |
-
try:
|
|
|
|
|
|
|
| 114 |
except:
|
| 115 |
try: os.killpg(os.getpgid(pid), signal.SIGKILL)
|
| 116 |
except: pass
|
| 117 |
-
self.process = None
|
| 118 |
-
|
|
|
|
|
|
|
| 119 |
|
| 120 |
@property
|
| 121 |
def is_running(self): return self.process is not None and self.process.poll() is None
|
|
@@ -479,8 +573,12 @@ def ai_gen(system_prompt, code, max_tokens=300):
|
|
| 479 |
# ═══════════════════════════════════════
|
| 480 |
# GUI code detection
|
| 481 |
# ═══════════════════════════════════════
|
| 482 |
-
GUI_HINTS = [
|
| 483 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
def is_gui_code(c): return any(h in c for h in GUI_HINTS)
|
| 485 |
|
| 486 |
# ═══════════════════════════════════════
|
|
@@ -689,10 +787,13 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 689 |
display_image = gr.Image(label="", type="filepath", interactive=False, height=420)
|
| 690 |
with gr.Row():
|
| 691 |
capture_btn = gr.Button("📸 CAPTURE", size="sm", elem_classes="tb")
|
| 692 |
-
auto_capture = gr.Checkbox(label="Auto
|
|
|
|
|
|
|
|
|
|
| 693 |
gui_status = gr.Markdown(
|
| 694 |
f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} "
|
| 695 |
-
f"| {SCREEN_W}x{SCREEN_H}</small>")
|
| 696 |
|
| 697 |
status_bar = gr.Markdown(
|
| 698 |
f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "
|
|
@@ -702,7 +803,7 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 702 |
# State
|
| 703 |
diff_original = gr.State("")
|
| 704 |
diff_modified = gr.State("")
|
| 705 |
-
auto_timer = gr.Timer(
|
| 706 |
|
| 707 |
# ═══════════════════════════════════════
|
| 708 |
# HANDLERS
|
|
@@ -745,9 +846,12 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 745 |
fs.save_file(code)
|
| 746 |
|
| 747 |
if is_gui_code(code):
|
| 748 |
-
# GUI app — launch
|
| 749 |
msg = gui_mgr.launch(code)
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
| 751 |
|
| 752 |
# Normal script — run with subprocess, output goes to OUTPUT tab
|
| 753 |
tmp = "/tmp/_axon_run.py"
|
|
@@ -768,11 +872,12 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 768 |
output = "[Timed out after 30s]"
|
| 769 |
except Exception as e:
|
| 770 |
output = f"[Error] {e}"
|
| 771 |
-
return output, gr.update(), gr.update()
|
| 772 |
|
| 773 |
def on_stop():
|
| 774 |
-
msg = gui_mgr.stop()
|
| 775 |
-
|
|
|
|
| 776 |
|
| 777 |
# --- Terminal ---
|
| 778 |
def on_term_cmd(cmd):
|
|
@@ -851,12 +956,19 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 851 |
return vdisplay.capture()
|
| 852 |
|
| 853 |
def on_auto_tick():
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
|
|
|
|
|
|
| 857 |
|
| 858 |
def on_auto_toggle(checked):
|
| 859 |
-
return gr.Timer(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 860 |
|
| 861 |
# ═══════════════════════════════════════
|
| 862 |
# WIRING
|
|
@@ -870,9 +982,9 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 870 |
del_btn.click(on_delete, None, [file_list, editor, editor, structure_view])
|
| 871 |
|
| 872 |
# Run
|
| 873 |
-
run_btn.click(on_run, editor, [run_output, display_image, gui_status]
|
| 874 |
).then(lambda: gr.Tabs(selected="output-tab"), None, bottom_tabs)
|
| 875 |
-
stop_btn.click(on_stop, None, [
|
| 876 |
|
| 877 |
# Terminal
|
| 878 |
term_in.submit(on_term_cmd, term_in, [term_out, term_in])
|
|
@@ -914,6 +1026,7 @@ with gr.Blocks(title="Axon Pro") as demo:
|
|
| 914 |
# Display
|
| 915 |
capture_btn.click(on_capture, None, display_image)
|
| 916 |
auto_capture.change(on_auto_toggle, auto_capture, auto_timer)
|
|
|
|
| 917 |
auto_timer.tick(on_auto_tick, None, display_image)
|
| 918 |
|
| 919 |
|
|
|
|
| 30 |
SNIPPETS_FILE = "/tmp/axon_snippets.json"
|
| 31 |
|
| 32 |
# ═══════════════════════════════════════
|
| 33 |
+
# Virtual Display (Optimized for pygame/tkinter/turtle)
|
| 34 |
# ═══════════════════════════════════════
|
| 35 |
class VirtualDisplay:
|
| 36 |
def __init__(self):
|
| 37 |
self.xvfb_proc = None
|
| 38 |
self.display = DISPLAY_NUM
|
| 39 |
+
self._last_capture = None
|
| 40 |
self._start_xvfb()
|
| 41 |
|
| 42 |
def _start_xvfb(self):
|
| 43 |
try:
|
| 44 |
+
subprocess.run(["pkill", "-f", f"Xvfb {self.display}"],
|
| 45 |
+
capture_output=True, timeout=5)
|
| 46 |
+
time.sleep(0.3)
|
| 47 |
+
# Full-featured Xvfb: GLX for OpenGL, RENDER for anti-aliasing,
|
| 48 |
+
# 24-bit color, no access control
|
| 49 |
+
self.xvfb_proc = subprocess.Popen([
|
| 50 |
+
"Xvfb", self.display,
|
| 51 |
+
"-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24",
|
| 52 |
+
"-ac", # No access control
|
| 53 |
+
"-nolisten", "tcp", # Security
|
| 54 |
+
"+extension", "GLX", # OpenGL support (pygame)
|
| 55 |
+
"+extension", "RENDER", # Anti-aliased rendering
|
| 56 |
+
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 57 |
+
time.sleep(0.8)
|
| 58 |
if self.xvfb_proc.poll() is None:
|
|
|
|
| 59 |
os.environ["DISPLAY"] = self.display
|
| 60 |
+
print(f"[Xvfb] Running on {self.display} ({SCREEN_W}x{SCREEN_H})")
|
| 61 |
else:
|
| 62 |
+
# Fallback: simpler Xvfb without extensions
|
| 63 |
+
self.xvfb_proc = subprocess.Popen([
|
| 64 |
+
"Xvfb", self.display,
|
| 65 |
+
"-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24", "-ac",
|
| 66 |
+
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 67 |
+
time.sleep(0.5)
|
| 68 |
+
if self.xvfb_proc.poll() is None:
|
| 69 |
+
os.environ["DISPLAY"] = self.display
|
| 70 |
+
print(f"[Xvfb] Running (fallback mode)")
|
| 71 |
+
else:
|
| 72 |
+
self.xvfb_proc = None
|
| 73 |
+
print("[Xvfb] Failed to start")
|
| 74 |
except Exception as e:
|
| 75 |
print(f"[Xvfb] {e}"); self.xvfb_proc = None
|
| 76 |
|
| 77 |
def capture(self):
|
| 78 |
+
"""Capture the virtual display. Returns filepath or None."""
|
| 79 |
if not self.is_running: return None
|
| 80 |
+
|
| 81 |
+
# Clean up previous capture to avoid /tmp filling up
|
| 82 |
+
if self._last_capture:
|
| 83 |
+
try: os.unlink(self._last_capture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
except: pass
|
| 85 |
+
|
| 86 |
+
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
| 87 |
+
tmp.close()
|
| 88 |
+
cap_env = {**os.environ, "DISPLAY": self.display}
|
| 89 |
+
|
| 90 |
+
# Method 1: xwd + convert (most reliable for Xvfb)
|
| 91 |
+
try:
|
| 92 |
+
xwd = subprocess.run(
|
| 93 |
+
["xwd", "-root", "-display", self.display, "-silent"],
|
| 94 |
+
capture_output=True, timeout=3, env=cap_env)
|
| 95 |
+
if xwd.returncode == 0 and xwd.stdout:
|
| 96 |
+
conv = subprocess.run(
|
| 97 |
+
["convert", "xwd:-", tmp.name],
|
| 98 |
+
input=xwd.stdout, capture_output=True, timeout=3)
|
| 99 |
+
if conv.returncode == 0 and os.path.getsize(tmp.name) > 100:
|
| 100 |
+
self._last_capture = tmp.name
|
| 101 |
+
return tmp.name
|
| 102 |
+
except: pass
|
| 103 |
+
|
| 104 |
+
# Method 2: scrot
|
| 105 |
+
try:
|
| 106 |
+
r = subprocess.run(
|
| 107 |
+
["scrot", tmp.name],
|
| 108 |
+
capture_output=True, timeout=3, env=cap_env)
|
| 109 |
+
if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:
|
| 110 |
+
self._last_capture = tmp.name
|
| 111 |
+
return tmp.name
|
| 112 |
+
except: pass
|
| 113 |
+
|
| 114 |
+
# Method 3: import (ImageMagick)
|
| 115 |
+
try:
|
| 116 |
+
r = subprocess.run(
|
| 117 |
+
f"import -window root -display {self.display} {tmp.name}",
|
| 118 |
+
shell=True, capture_output=True, timeout=3, env=cap_env)
|
| 119 |
+
if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:
|
| 120 |
+
self._last_capture = tmp.name
|
| 121 |
+
return tmp.name
|
| 122 |
+
except: pass
|
| 123 |
+
|
| 124 |
try: os.unlink(tmp.name)
|
| 125 |
except: pass
|
| 126 |
return None
|
| 127 |
|
| 128 |
@property
|
| 129 |
+
def is_running(self):
|
| 130 |
+
return self.xvfb_proc is not None and self.xvfb_proc.poll() is None
|
| 131 |
+
|
| 132 |
def cleanup(self):
|
| 133 |
+
if self._last_capture:
|
| 134 |
+
try: os.unlink(self._last_capture)
|
| 135 |
+
except: pass
|
| 136 |
if self.xvfb_proc:
|
| 137 |
self.xvfb_proc.terminate()
|
| 138 |
try: self.xvfb_proc.wait(timeout=3)
|
|
|
|
| 141 |
vdisplay = VirtualDisplay()
|
| 142 |
|
| 143 |
# ═══════════════════════════════════════
|
| 144 |
+
# GUI Process Manager (pygame/tkinter/turtle)
|
| 145 |
# ═══════════════════════════════════════
|
| 146 |
+
GUI_LOG = "/tmp/_axon_gui.log"
|
| 147 |
+
|
| 148 |
class GUIProcessManager:
|
| 149 |
def __init__(self):
|
| 150 |
self.process = None
|
|
|
|
| 153 |
self.stop()
|
| 154 |
tmp = "/tmp/_axon_gui_run.py"
|
| 155 |
with open(tmp, "w") as f: f.write(code)
|
| 156 |
+
|
| 157 |
+
# Full environment for GUI frameworks
|
| 158 |
env = os.environ.copy()
|
| 159 |
+
env.update({
|
| 160 |
+
"DISPLAY": DISPLAY_NUM,
|
| 161 |
+
# SDL / pygame
|
| 162 |
+
"SDL_VIDEODRIVER": "x11",
|
| 163 |
+
"SDL_AUDIODRIVER": "dummy", # No audio device in headless
|
| 164 |
+
"PYGAME_HIDE_SUPPORT_PROMPT": "1", # Suppress pygame welcome
|
| 165 |
+
# Tkinter / general X11
|
| 166 |
+
"XDG_RUNTIME_DIR": "/tmp",
|
| 167 |
+
"MESA_GL_VERSION_OVERRIDE": "3.3", # OpenGL compat
|
| 168 |
+
"LIBGL_ALWAYS_SOFTWARE": "1", # Software rendering (no GPU)
|
| 169 |
+
# Python
|
| 170 |
+
"PYTHONUNBUFFERED": "1",
|
| 171 |
+
"PYTHONPATH": "/tmp/axon_workspace:" + env.get("PYTHONPATH", ""),
|
| 172 |
+
})
|
| 173 |
+
|
| 174 |
try:
|
| 175 |
+
# Log file instead of PIPE — PIPE deadlocks pygame's output
|
| 176 |
+
log_f = open(GUI_LOG, "w")
|
| 177 |
+
self.process = subprocess.Popen(
|
| 178 |
+
[sys.executable, "-u", tmp], # -u = unbuffered
|
| 179 |
+
stdout=log_f, stderr=log_f,
|
| 180 |
+
env=env, preexec_fn=os.setsid, cwd="/tmp/axon_workspace")
|
| 181 |
+
|
| 182 |
+
time.sleep(1.0) # Give pygame time to create the window
|
| 183 |
+
|
| 184 |
if self.process.poll() is not None:
|
| 185 |
+
log_f.close()
|
| 186 |
+
try:
|
| 187 |
+
with open(GUI_LOG) as f: msg = f.read().strip()
|
| 188 |
+
except: msg = ""
|
| 189 |
+
return f"[Exited immediately]\n{msg}" if msg else "[Exited — no output]"
|
| 190 |
+
|
| 191 |
+
return f"[GUI launched — PID {self.process.pid}] Auto-refresh enabled."
|
| 192 |
+
except Exception as e:
|
| 193 |
+
return f"[Error] {e}"
|
| 194 |
+
|
| 195 |
+
def get_log(self):
|
| 196 |
+
try:
|
| 197 |
+
with open(GUI_LOG) as f: return f.read()[-2000:]
|
| 198 |
+
except: return ""
|
| 199 |
|
| 200 |
def stop(self):
|
| 201 |
if self.process and self.process.poll() is None:
|
| 202 |
pid = self.process.pid
|
| 203 |
+
try:
|
| 204 |
+
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
| 205 |
+
self.process.wait(timeout=3)
|
| 206 |
except:
|
| 207 |
try: os.killpg(os.getpgid(pid), signal.SIGKILL)
|
| 208 |
except: pass
|
| 209 |
+
self.process = None
|
| 210 |
+
return f"[Stopped PID {pid}]"
|
| 211 |
+
self.process = None
|
| 212 |
+
return "[No process]"
|
| 213 |
|
| 214 |
@property
|
| 215 |
def is_running(self): return self.process is not None and self.process.poll() is None
|
|
|
|
| 573 |
# ═══════════════════════════════════════
|
| 574 |
# GUI code detection
|
| 575 |
# ═══════════════════════════════════════
|
| 576 |
+
GUI_HINTS = [
|
| 577 |
+
"pygame", "tkinter", "turtle", "pyglet", "arcade", "kivy",
|
| 578 |
+
"display.set_mode", "mainloop()", "Tk()", "Canvas(",
|
| 579 |
+
"pygame.init", "pygame.display", "screen.fill",
|
| 580 |
+
"from turtle import", "import turtle",
|
| 581 |
+
]
|
| 582 |
def is_gui_code(c): return any(h in c for h in GUI_HINTS)
|
| 583 |
|
| 584 |
# ═══════════════════════════════════════
|
|
|
|
| 787 |
display_image = gr.Image(label="", type="filepath", interactive=False, height=420)
|
| 788 |
with gr.Row():
|
| 789 |
capture_btn = gr.Button("📸 CAPTURE", size="sm", elem_classes="tb")
|
| 790 |
+
auto_capture = gr.Checkbox(label="Auto Refresh", value=False)
|
| 791 |
+
refresh_speed = gr.Dropdown(
|
| 792 |
+
choices=["0.5s", "1s", "2s", "5s"], value="1s",
|
| 793 |
+
label="", container=False, scale=1, interactive=True)
|
| 794 |
gui_status = gr.Markdown(
|
| 795 |
f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} "
|
| 796 |
+
f"| {SCREEN_W}x{SCREEN_H} | SDL: x11 + dummy audio</small>")
|
| 797 |
|
| 798 |
status_bar = gr.Markdown(
|
| 799 |
f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "
|
|
|
|
| 803 |
# State
|
| 804 |
diff_original = gr.State("")
|
| 805 |
diff_modified = gr.State("")
|
| 806 |
+
auto_timer = gr.Timer(1, active=False)
|
| 807 |
|
| 808 |
# ═══════════════════════════════════════
|
| 809 |
# HANDLERS
|
|
|
|
| 846 |
fs.save_file(code)
|
| 847 |
|
| 848 |
if is_gui_code(code):
|
| 849 |
+
# GUI app — launch, take first screenshot, auto-enable refresh
|
| 850 |
msg = gui_mgr.launch(code)
|
| 851 |
+
time.sleep(0.5) # Extra time for first frame
|
| 852 |
+
ss = vdisplay.capture()
|
| 853 |
+
status = f"<small>Xvfb: ● ON | {gui_mgr.get_status()}</small>"
|
| 854 |
+
return msg, ss, status, gr.update(value=True) # Enable auto-refresh
|
| 855 |
|
| 856 |
# Normal script — run with subprocess, output goes to OUTPUT tab
|
| 857 |
tmp = "/tmp/_axon_run.py"
|
|
|
|
| 872 |
output = "[Timed out after 30s]"
|
| 873 |
except Exception as e:
|
| 874 |
output = f"[Error] {e}"
|
| 875 |
+
return output, gr.update(), gr.update(), gr.update()
|
| 876 |
|
| 877 |
def on_stop():
|
| 878 |
+
msg = gui_mgr.stop()
|
| 879 |
+
status = f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} | ○ idle</small>"
|
| 880 |
+
return msg, status, gr.update(value=False) # Disable auto-refresh
|
| 881 |
|
| 882 |
# --- Terminal ---
|
| 883 |
def on_term_cmd(cmd):
|
|
|
|
| 956 |
return vdisplay.capture()
|
| 957 |
|
| 958 |
def on_auto_tick():
|
| 959 |
+
ss = vdisplay.capture()
|
| 960 |
+
# Auto-disable if GUI process died
|
| 961 |
+
if not gui_mgr.is_running:
|
| 962 |
+
return ss if ss else gr.update()
|
| 963 |
+
return ss if ss else gr.update()
|
| 964 |
|
| 965 |
def on_auto_toggle(checked):
|
| 966 |
+
return gr.Timer(1, active=checked)
|
| 967 |
+
|
| 968 |
+
def on_speed_change(speed, auto_on):
|
| 969 |
+
speed_map = {"0.5s": 0.5, "1s": 1, "2s": 2, "5s": 5}
|
| 970 |
+
interval = speed_map.get(speed, 1)
|
| 971 |
+
return gr.Timer(interval, active=auto_on)
|
| 972 |
|
| 973 |
# ═══════════════════════════════════════
|
| 974 |
# WIRING
|
|
|
|
| 982 |
del_btn.click(on_delete, None, [file_list, editor, editor, structure_view])
|
| 983 |
|
| 984 |
# Run
|
| 985 |
+
run_btn.click(on_run, editor, [run_output, display_image, gui_status, auto_capture]
|
| 986 |
).then(lambda: gr.Tabs(selected="output-tab"), None, bottom_tabs)
|
| 987 |
+
stop_btn.click(on_stop, None, [run_output, gui_status, auto_capture])
|
| 988 |
|
| 989 |
# Terminal
|
| 990 |
term_in.submit(on_term_cmd, term_in, [term_out, term_in])
|
|
|
|
| 1026 |
# Display
|
| 1027 |
capture_btn.click(on_capture, None, display_image)
|
| 1028 |
auto_capture.change(on_auto_toggle, auto_capture, auto_timer)
|
| 1029 |
+
refresh_speed.change(on_speed_change, [refresh_speed, auto_capture], auto_timer)
|
| 1030 |
auto_timer.tick(on_auto_tick, None, display_image)
|
| 1031 |
|
| 1032 |
|