THEZYZSTUDIO commited on
Commit
eb6c668
Β·
verified Β·
1 Parent(s): 32fa524

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -141
app.py CHANGED
@@ -1,12 +1,10 @@
1
  """
2
- THE Z AI β€” Computer Mode Server
3
- ================================
4
- سيرفر Ψ§Ω„ΨͺΨ­ΩƒΩ… Ψ¨Ψ§Ω„ΩƒΩ…Ψ¨ΩŠΩˆΨͺΨ± ΨΉΩ† Ψ¨ΨΉΨ― Ω„Ω„Ψ°ΩƒΨ§Ψ‘ Ψ§Ω„Ψ§Ψ΅Ψ·Ω†Ψ§ΨΉΩŠ
5
- يوفر:
6
- - WebSocket Ω„Ω„ΨͺΨ­ΩƒΩ… Ψ§Ω„Ω„Ψ­ΨΈΩŠ (Ψ§Ω„Ω…Ψ§ΩˆΨ³ΨŒ Ω„ΩˆΨ­Ψ© المفاΨͺيح، Ψ§Ω„ΨͺΨ±Ω…ΩŠΩ†Ψ§Ω„)
7
- - Ψͺءوير Ψ³ΩƒΨ±ΩŠΩ† شوΨͺ وΨ₯Ψ±Ψ³Ψ§Ω„Ω‡ Ω„Ω„ΨΉΩ…ΩŠΩ„
8
- - Ψ¨Ψ« Ψ΄Ψ§Ψ΄Ψ© Ω…Ψ¨Ψ§Ψ΄Ψ± (base64 frames)
9
- - ΨͺΩ†ΩΩŠΨ° Ψ£ΩˆΨ§Ω…Ψ± bash ΩˆΩ‚Ψ±Ψ§Ψ‘Ψ© Ψ§Ω„Ω†Ψ§ΨͺΨ¬
10
  """
11
 
12
  import asyncio
@@ -15,14 +13,9 @@ import io
15
  import json
16
  import os
17
  import subprocess
18
- import threading
19
  import time
20
  import traceback
21
- from pathlib import Path
22
 
23
- import pyautogui
24
- import pyperclip
25
- from PIL import Image, ImageGrab
26
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
27
  from fastapi.middleware.cors import CORSMiddleware
28
  from fastapi.responses import JSONResponse
@@ -31,11 +24,8 @@ import uvicorn
31
  # ─── ENV ────────────────────────────────────────────
32
  DISPLAY = os.environ.get("DISPLAY", ":1")
33
  os.environ["DISPLAY"] = DISPLAY
34
- pyautogui.FAILSAFE = False
35
- pyautogui.PAUSE = 0.05
36
-
37
- app = FastAPI(title="Z-Computer-Mode API")
38
 
 
39
  app.add_middleware(
40
  CORSMiddleware,
41
  allow_origins=["*"],
@@ -44,20 +34,67 @@ app.add_middleware(
44
  allow_headers=["*"],
45
  )
46
 
47
- # ─── Active Connections ──────────────────────────────
48
  active_connections: list[WebSocket] = []
49
  stream_active = False
50
- stream_quality = 60 # JPEG quality
51
- stream_fps = 3 # frames per second (low to save bandwidth)
52
- stream_scale = 0.5 # downscale factor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
 
 
 
 
 
 
54
 
55
- # ─── Utility ─────────────────────────────────────────
56
 
57
- def capture_screen(scale=stream_scale, quality=stream_quality) -> str:
58
- """Capture screen β†’ base64 JPEG string"""
59
  try:
60
- img = ImageGrab.grab()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  if scale < 1.0:
62
  w = int(img.width * scale)
63
  h = int(img.height * scale)
@@ -66,52 +103,62 @@ def capture_screen(scale=stream_scale, quality=stream_quality) -> str:
66
  img.convert("RGB").save(buf, format="JPEG", quality=quality, optimize=True)
67
  return base64.b64encode(buf.getvalue()).decode()
68
  except Exception as e:
 
69
  return ""
70
 
71
 
 
 
 
 
 
 
 
 
 
 
 
72
  def run_command(cmd: str, timeout: int = 30) -> dict:
73
- """Run a shell command and return stdout/stderr"""
74
  try:
75
  result = subprocess.run(
76
  cmd, shell=True, capture_output=True, text=True,
77
  timeout=timeout, env={**os.environ, "DISPLAY": DISPLAY}
78
  )
79
  return {
80
- "stdout": result.stdout[-8000:] if len(result.stdout) > 8000 else result.stdout,
81
- "stderr": result.stderr[-4000:] if len(result.stderr) > 4000 else result.stderr,
82
  "returncode": result.returncode,
83
  }
84
  except subprocess.TimeoutExpired:
85
- return {"stdout": "", "stderr": f"Command timed out after {timeout}s", "returncode": -1}
86
  except Exception as e:
87
  return {"stdout": "", "stderr": str(e), "returncode": -1}
88
 
89
 
90
  async def broadcast(msg: dict):
91
- """Send JSON message to all connected WebSocket clients"""
92
  txt = json.dumps(msg)
93
- disconnected = []
94
  for ws in active_connections:
95
  try:
96
  await ws.send_text(txt)
97
- except Exception:
98
- disconnected.append(ws)
99
- for ws in disconnected:
100
  if ws in active_connections:
101
  active_connections.remove(ws)
102
 
103
 
104
- # ─── Screen Streaming Loop ───────────────────────────
105
 
106
  async def screen_stream_loop():
107
  global stream_active
108
- interval = 1.0 / stream_fps
109
  while stream_active and active_connections:
110
  try:
111
- frame = capture_screen()
112
  if frame:
113
  await broadcast({"type": "frame", "data": frame})
114
- except Exception:
115
  pass
116
  await asyncio.sleep(interval)
117
  stream_active = False
@@ -121,107 +168,120 @@ async def screen_stream_loop():
121
 
122
  async def handle_action(ws: WebSocket, msg: dict):
123
  action = msg.get("action", "")
124
- data = msg.get("data", {})
 
 
 
125
 
126
- # ── SCREENSHOT ──────────────────────────────────
 
 
 
 
 
 
 
127
  if action == "screenshot":
128
  frame = capture_screen(scale=0.8, quality=75)
129
- await ws.send_text(json.dumps({
130
- "type": "screenshot",
131
- "data": frame,
132
- "ts": int(time.time() * 1000)
133
- }))
134
 
135
- # ── TERMINAL COMMAND ─────────────────────────────
136
  elif action == "terminal":
137
  cmd = data.get("cmd", "")
138
  if cmd:
139
- result = run_command(cmd, timeout=data.get("timeout", 30))
140
- await ws.send_text(json.dumps({
141
  "type": "terminal_result",
142
  "cmd": cmd,
143
- "stdout": result["stdout"],
144
- "stderr": result["stderr"],
145
- "returncode": result["returncode"],
146
- }))
147
- # Auto-screenshot after terminal command
148
- await asyncio.sleep(0.5)
149
- frame = capture_screen(scale=0.7, quality=70)
150
- if frame:
151
- await ws.send_text(json.dumps({
152
- "type": "screenshot",
153
- "data": frame,
154
- "ts": int(time.time() * 1000),
155
- "auto": True
156
- }))
157
-
158
- # ── MOUSE MOVE ───────────────────────────────────
159
  elif action == "mouse_move":
160
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
161
- pyautogui.moveTo(x, y, duration=0.1)
162
- await ws.send_text(json.dumps({"type": "ack", "action": "mouse_move"}))
163
 
164
- # ���─ MOUSE CLICK ──────────────────────────────────
165
  elif action == "mouse_click":
166
- x = int(data.get("x", 0))
167
- y = int(data.get("y", 0))
168
  button = data.get("button", "left")
 
169
  double = data.get("double", False)
170
- pyautogui.moveTo(x, y, duration=0.08)
 
171
  if double:
172
- pyautogui.doubleClick(x, y, button=button)
173
  else:
174
- pyautogui.click(x, y, button=button)
175
- await ws.send_text(json.dumps({"type": "ack", "action": "mouse_click"}))
 
176
 
177
- # ── MOUSE DRAG ───────────────────────────────────
178
  elif action == "mouse_drag":
179
  x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
180
  x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
181
- pyautogui.moveTo(x1, y1)
182
- pyautogui.dragTo(x2, y2, duration=0.3, button="left")
183
- await ws.send_text(json.dumps({"type": "ack", "action": "mouse_drag"}))
184
-
185
- # ── KEYBOARD TYPE ────────────────────────────────
 
 
 
 
186
  elif action == "keyboard_type":
187
  text = data.get("text", "")
188
- pyautogui.typewrite(text, interval=0.03)
189
- await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_type"}))
 
 
190
 
191
- # ── KEYBOARD HOTKEY ──────────────────────────────
192
  elif action == "keyboard_hotkey":
193
  keys = data.get("keys", [])
194
  if keys:
195
- pyautogui.hotkey(*keys)
196
- await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_hotkey"}))
 
197
 
198
- # ── KEYBOARD PRESS ───────────────────────────────
199
  elif action == "keyboard_press":
200
  key = data.get("key", "")
201
  if key:
202
- pyautogui.press(key)
203
- await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_press"}))
204
 
205
- # ── CLIPBOARD WRITE ──────────────────────────────
206
  elif action == "clipboard_write":
207
  text = data.get("text", "")
208
- pyperclip.copy(text)
209
- await ws.send_text(json.dumps({"type": "ack", "action": "clipboard_write"}))
 
 
 
 
 
210
 
211
- # ── CLIPBOARD READ ───────────────────────────────
212
  elif action == "clipboard_read":
213
- text = pyperclip.paste()
214
- await ws.send_text(json.dumps({"type": "clipboard_content", "text": text}))
215
 
216
- # ── SCROLL ───────────────────────────────────────
217
  elif action == "scroll":
218
- x = int(data.get("x", 0))
219
- y = int(data.get("y", 0))
220
  clicks = int(data.get("clicks", 3))
221
- pyautogui.scroll(clicks, x=x, y=y)
222
- await ws.send_text(json.dumps({"type": "ack", "action": "scroll"}))
 
 
 
223
 
224
- # ── OPEN APP ─────────────────────────────────────
225
  elif action == "open_app":
226
  app_cmd = data.get("cmd", "")
227
  if app_cmd:
@@ -230,49 +290,51 @@ async def handle_action(ws: WebSocket, msg: dict):
230
  env={**os.environ, "DISPLAY": DISPLAY},
231
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
232
  )
233
- await ws.send_text(json.dumps({"type": "ack", "action": "open_app"}))
234
- # Auto-screenshot after app opens (wait for it to load)
235
- await asyncio.sleep(3)
236
- frame = capture_screen(scale=0.7, quality=70)
237
- if frame:
238
- await ws.send_text(json.dumps({
239
- "type": "screenshot",
240
- "data": frame,
241
- "ts": int(time.time() * 1000),
242
- "auto": True
243
- }))
244
-
245
- # ── START STREAM ─────────────────────────────────
246
  elif action == "start_stream":
247
  global stream_active, stream_fps, stream_quality, stream_scale
248
- stream_fps = int(data.get("fps", 3))
249
  stream_quality = int(data.get("quality", 60))
250
- stream_scale = float(data.get("scale", 0.5))
251
  if not stream_active:
252
  stream_active = True
253
  asyncio.create_task(screen_stream_loop())
254
- await ws.send_text(json.dumps({"type": "ack", "action": "start_stream"}))
255
 
256
- # ── STOP STREAM ──────────────────────────────────
257
  elif action == "stop_stream":
258
  stream_active = False
259
- await ws.send_text(json.dumps({"type": "ack", "action": "stop_stream"}))
260
 
261
- # ── GET SCREEN INFO ──────────────────────────────
262
  elif action == "screen_info":
 
263
  try:
264
- w, h = pyautogui.size()
265
- mx, my = pyautogui.position()
266
  except:
267
- w, h, mx, my = 1920, 1080, 0, 0
268
- await ws.send_text(json.dumps({
269
- "type": "screen_info",
270
- "width": w, "height": h,
271
- "mouse_x": mx, "mouse_y": my,
272
- }))
 
 
 
 
273
 
274
  else:
275
- await ws.send_text(json.dumps({"type": "error", "msg": f"Unknown action: {action}"}))
276
 
277
 
278
  # ─── WebSocket Endpoint ──────────────────────────────
@@ -281,17 +343,25 @@ async def handle_action(ws: WebSocket, msg: dict):
281
  async def websocket_endpoint(ws: WebSocket):
282
  await ws.accept()
283
  active_connections.append(ws)
284
- # Send welcome
 
285
  try:
286
- w, h = pyautogui.size()
 
287
  except:
288
  w, h = 1920, 1080
289
  await ws.send_text(json.dumps({
290
  "type": "connected",
291
- "screen_width": w,
292
- "screen_height": h,
293
- "msg": "Z Computer Mode β€” Connected"
294
  }))
 
 
 
 
 
 
 
295
  try:
296
  while True:
297
  raw = await ws.receive_text()
@@ -300,13 +370,13 @@ async def websocket_endpoint(ws: WebSocket):
300
  except WebSocketDisconnect:
301
  pass
302
  except Exception as e:
303
- pass
304
  finally:
305
  if ws in active_connections:
306
  active_connections.remove(ws)
307
 
308
 
309
- # ─── REST Fallback ───────────────────────────────────
310
 
311
  @app.get("/screenshot")
312
  async def rest_screenshot():
@@ -316,18 +386,19 @@ async def rest_screenshot():
316
 
317
  @app.post("/terminal")
318
  async def rest_terminal(body: dict):
319
- cmd = body.get("cmd", "")
320
- result = run_command(cmd, timeout=body.get("timeout", 30))
321
- return JSONResponse(result)
322
 
323
 
324
  @app.get("/health")
325
  async def health():
326
- return {"status": "ok", "display": DISPLAY}
327
-
 
 
328
 
329
- # ─── Entry ───────────────────────────────────────────
330
 
331
  if __name__ == "__main__":
332
  port = int(os.environ.get("PORT", 7860))
333
- uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="warning")
 
1
  """
2
+ THE Z AI β€” Computer Mode Server (FIXED v2)
3
+ ==========================================
4
+ Ψ₯Ψ΅Ω„Ψ§Ψ­Ψ§Ψͺ:
5
+ - Ψ§Ω„ΨͺΩ‚Ψ§Ψ· Ψ§Ω„Ψ΄Ψ§Ψ΄Ψ© Ψ§Ω„Ψ­Ω‚ΩŠΩ‚ΩŠ ΨΉΨ¨Ψ± scrot (ΩŠΨΉΩ…Ω„ Ω…ΨΉ Xvfb)
6
+ - فΨͺΨ­ Ψ§Ω„ΨͺΨ·Ψ¨ΩŠΩ‚Ψ§Ψͺ Ω…ΨΉ Ψ§Ω†ΨͺΨΈΨ§Ψ± Ψ­Ω‚ΩŠΩ‚ΩŠ
7
+ - ΨͺΨ³Ψ¬ΩŠΩ„ Ψ―Ω‚ΩŠΩ‚ Ω„Ω…Ψ§ يحدث فعلاً
 
 
8
  """
9
 
10
  import asyncio
 
13
  import json
14
  import os
15
  import subprocess
 
16
  import time
17
  import traceback
 
18
 
 
 
 
19
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from fastapi.responses import JSONResponse
 
24
  # ─── ENV ────────────────────────────────────────────
25
  DISPLAY = os.environ.get("DISPLAY", ":1")
26
  os.environ["DISPLAY"] = DISPLAY
 
 
 
 
27
 
28
+ app = FastAPI(title="Z-Computer-Mode API v2")
29
  app.add_middleware(
30
  CORSMiddleware,
31
  allow_origins=["*"],
 
34
  allow_headers=["*"],
35
  )
36
 
 
37
  active_connections: list[WebSocket] = []
38
  stream_active = False
39
+ stream_fps = 3
40
+ stream_quality = 60
41
+ stream_scale = 0.5
42
+
43
+
44
+ # ─── Screen Capture via scrot (works with Xvfb) ────
45
+
46
+ def capture_screen(scale=0.5, quality=60) -> str:
47
+ """Capture real screen using scrot β†’ base64 JPEG"""
48
+ try:
49
+ tmp = f"/tmp/zscreen_{int(time.time()*1000)}.png"
50
+ result = subprocess.run(
51
+ ["scrot", "-q", "90", tmp],
52
+ env={**os.environ, "DISPLAY": DISPLAY},
53
+ timeout=8, capture_output=True
54
+ )
55
+ if result.returncode != 0 or not os.path.exists(tmp):
56
+ # fallback: xwd β†’ PIL
57
+ return capture_screen_xwd(scale, quality)
58
+
59
+ from PIL import Image
60
+ img = Image.open(tmp)
61
+ os.unlink(tmp)
62
+
63
+ if scale < 1.0:
64
+ w = int(img.width * scale)
65
+ h = int(img.height * scale)
66
+ img = img.resize((w, h), Image.LANCZOS)
67
 
68
+ buf = io.BytesIO()
69
+ img.convert("RGB").save(buf, format="JPEG", quality=quality, optimize=True)
70
+ return base64.b64encode(buf.getvalue()).decode()
71
+ except Exception as e:
72
+ print(f"[capture_screen] error: {e}")
73
+ return ""
74
 
 
75
 
76
+ def capture_screen_xwd(scale=0.5, quality=60) -> str:
77
+ """Fallback: use xwd + PIL"""
78
  try:
79
+ tmp_xwd = f"/tmp/zscreen_{int(time.time()*1000)}.xwd"
80
+ tmp_png = tmp_xwd.replace(".xwd", ".png")
81
+ subprocess.run(
82
+ ["xwd", "-root", "-silent", "-out", tmp_xwd],
83
+ env={**os.environ, "DISPLAY": DISPLAY},
84
+ timeout=8, capture_output=True
85
+ )
86
+ subprocess.run(
87
+ ["convert", tmp_xwd, tmp_png],
88
+ timeout=8, capture_output=True
89
+ )
90
+ from PIL import Image
91
+ if not os.path.exists(tmp_png):
92
+ return ""
93
+ img = Image.open(tmp_png)
94
+ for f in [tmp_xwd, tmp_png]:
95
+ try: os.unlink(f)
96
+ except: pass
97
+
98
  if scale < 1.0:
99
  w = int(img.width * scale)
100
  h = int(img.height * scale)
 
103
  img.convert("RGB").save(buf, format="JPEG", quality=quality, optimize=True)
104
  return base64.b64encode(buf.getvalue()).decode()
105
  except Exception as e:
106
+ print(f"[capture_screen_xwd] error: {e}")
107
  return ""
108
 
109
 
110
+ # ─── Mouse/Keyboard via xdotool ─────────────────────
111
+
112
+ def xdo(args: list, timeout=10) -> dict:
113
+ result = subprocess.run(
114
+ ["xdotool"] + args,
115
+ env={**os.environ, "DISPLAY": DISPLAY},
116
+ timeout=timeout, capture_output=True, text=True
117
+ )
118
+ return {"rc": result.returncode, "out": result.stdout, "err": result.stderr}
119
+
120
+
121
  def run_command(cmd: str, timeout: int = 30) -> dict:
 
122
  try:
123
  result = subprocess.run(
124
  cmd, shell=True, capture_output=True, text=True,
125
  timeout=timeout, env={**os.environ, "DISPLAY": DISPLAY}
126
  )
127
  return {
128
+ "stdout": result.stdout[-8000:],
129
+ "stderr": result.stderr[-4000:],
130
  "returncode": result.returncode,
131
  }
132
  except subprocess.TimeoutExpired:
133
+ return {"stdout": "", "stderr": f"Timeout after {timeout}s", "returncode": -1}
134
  except Exception as e:
135
  return {"stdout": "", "stderr": str(e), "returncode": -1}
136
 
137
 
138
  async def broadcast(msg: dict):
 
139
  txt = json.dumps(msg)
140
+ dead = []
141
  for ws in active_connections:
142
  try:
143
  await ws.send_text(txt)
144
+ except:
145
+ dead.append(ws)
146
+ for ws in dead:
147
  if ws in active_connections:
148
  active_connections.remove(ws)
149
 
150
 
151
+ # ─── Stream Loop ─────────────────────────────────────
152
 
153
  async def screen_stream_loop():
154
  global stream_active
155
+ interval = 1.0 / max(1, stream_fps)
156
  while stream_active and active_connections:
157
  try:
158
+ frame = capture_screen(scale=stream_scale, quality=stream_quality)
159
  if frame:
160
  await broadcast({"type": "frame", "data": frame})
161
+ except:
162
  pass
163
  await asyncio.sleep(interval)
164
  stream_active = False
 
168
 
169
  async def handle_action(ws: WebSocket, msg: dict):
170
  action = msg.get("action", "")
171
+ data = msg.get("data", {})
172
+
173
+ async def send(obj):
174
+ await ws.send_text(json.dumps(obj))
175
 
176
+ async def auto_screenshot(delay=0.8, sc=0.7, q=70):
177
+ await asyncio.sleep(delay)
178
+ frame = capture_screen(scale=sc, quality=q)
179
+ if frame:
180
+ await send({"type": "screenshot", "data": frame,
181
+ "ts": int(time.time() * 1000), "auto": True})
182
+
183
+ # ── SCREENSHOT ──
184
  if action == "screenshot":
185
  frame = capture_screen(scale=0.8, quality=75)
186
+ await send({"type": "screenshot", "data": frame, "ts": int(time.time() * 1000)})
 
 
 
 
187
 
188
+ # ── TERMINAL ──
189
  elif action == "terminal":
190
  cmd = data.get("cmd", "")
191
  if cmd:
192
+ res = run_command(cmd, timeout=data.get("timeout", 30))
193
+ await send({
194
  "type": "terminal_result",
195
  "cmd": cmd,
196
+ "stdout": res["stdout"],
197
+ "stderr": res["stderr"],
198
+ "returncode": res["returncode"],
199
+ })
200
+ await auto_screenshot(0.6)
201
+
202
+ # ── MOUSE MOVE ──
 
 
 
 
 
 
 
 
 
203
  elif action == "mouse_move":
204
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
205
+ xdo(["mousemove", str(x), str(y)])
206
+ await send({"type": "ack", "action": "mouse_move"})
207
 
208
+ # ── MOUSE CLICK ──
209
  elif action == "mouse_click":
210
+ x, y = int(data.get("x", 0)), int(data.get("y", 0))
 
211
  button = data.get("button", "left")
212
+ btn_num = {"left": "1", "middle": "2", "right": "3"}.get(button, "1")
213
  double = data.get("double", False)
214
+ xdo(["mousemove", str(x), str(y)])
215
+ await asyncio.sleep(0.05)
216
  if double:
217
+ xdo(["click", "--repeat", "2", "--delay", "100", btn_num])
218
  else:
219
+ xdo(["click", btn_num])
220
+ await send({"type": "ack", "action": "mouse_click"})
221
+ await auto_screenshot(0.5)
222
 
223
+ # ── MOUSE DRAG ──
224
  elif action == "mouse_drag":
225
  x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
226
  x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
227
+ xdo(["mousemove", str(x1), str(y1)])
228
+ xdo(["mousedown", "1"])
229
+ await asyncio.sleep(0.1)
230
+ xdo(["mousemove", str(x2), str(y2)])
231
+ await asyncio.sleep(0.1)
232
+ xdo(["mouseup", "1"])
233
+ await send({"type": "ack", "action": "mouse_drag"})
234
+
235
+ # ── KEYBOARD TYPE ──
236
  elif action == "keyboard_type":
237
  text = data.get("text", "")
238
+ if text:
239
+ # Use xdotool type for proper Unicode support
240
+ xdo(["type", "--clearmodifiers", "--delay", "30", text])
241
+ await send({"type": "ack", "action": "keyboard_type"})
242
 
243
+ # ── KEYBOARD HOTKEY ──
244
  elif action == "keyboard_hotkey":
245
  keys = data.get("keys", [])
246
  if keys:
247
+ combo = "+".join(keys)
248
+ xdo(["key", "--clearmodifiers", combo])
249
+ await send({"type": "ack", "action": "keyboard_hotkey"})
250
 
251
+ # ── KEYBOARD PRESS ──
252
  elif action == "keyboard_press":
253
  key = data.get("key", "")
254
  if key:
255
+ xdo(["key", "--clearmodifiers", key])
256
+ await send({"type": "ack", "action": "keyboard_press"})
257
 
258
+ # ── CLIPBOARD ──
259
  elif action == "clipboard_write":
260
  text = data.get("text", "")
261
+ proc = subprocess.Popen(
262
+ ["xclip", "-selection", "clipboard"],
263
+ stdin=subprocess.PIPE,
264
+ env={**os.environ, "DISPLAY": DISPLAY}
265
+ )
266
+ proc.communicate(text.encode())
267
+ await send({"type": "ack", "action": "clipboard_write"})
268
 
 
269
  elif action == "clipboard_read":
270
+ res = run_command("xclip -selection clipboard -o", timeout=5)
271
+ await send({"type": "clipboard_content", "text": res["stdout"]})
272
 
273
+ # ── SCROLL ──
274
  elif action == "scroll":
275
+ x = int(data.get("x", 0))
276
+ y = int(data.get("y", 0))
277
  clicks = int(data.get("clicks", 3))
278
+ btn = "4" if clicks > 0 else "5"
279
+ xdo(["mousemove", str(x), str(y)])
280
+ for _ in range(abs(clicks)):
281
+ xdo(["click", btn])
282
+ await send({"type": "ack", "action": "scroll"})
283
 
284
+ # ── OPEN APP ──
285
  elif action == "open_app":
286
  app_cmd = data.get("cmd", "")
287
  if app_cmd:
 
290
  env={**os.environ, "DISPLAY": DISPLAY},
291
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
292
  )
293
+ await send({"type": "ack", "action": "open_app", "cmd": app_cmd})
294
+ # Wait longer for app to open then take screenshot
295
+ await asyncio.sleep(4)
296
+ frame = capture_screen(scale=0.75, quality=72)
297
+ if frame:
298
+ await send({"type": "screenshot", "data": frame,
299
+ "ts": int(time.time() * 1000), "auto": True, "label": f"Ψ¨ΨΉΨ― فΨͺΨ­: {app_cmd}"})
300
+ else:
301
+ await send({"type": "ack", "action": "open_app"})
302
+
303
+ # ── STREAM ──
 
 
304
  elif action == "start_stream":
305
  global stream_active, stream_fps, stream_quality, stream_scale
306
+ stream_fps = int(data.get("fps", 3))
307
  stream_quality = int(data.get("quality", 60))
308
+ stream_scale = float(data.get("scale", 0.5))
309
  if not stream_active:
310
  stream_active = True
311
  asyncio.create_task(screen_stream_loop())
312
+ await send({"type": "ack", "action": "start_stream"})
313
 
 
314
  elif action == "stop_stream":
315
  stream_active = False
316
+ await send({"type": "ack", "action": "stop_stream"})
317
 
318
+ # ── SCREEN INFO ──
319
  elif action == "screen_info":
320
+ res = run_command("xdotool getdisplaygeometry", timeout=5)
321
  try:
322
+ parts = res["stdout"].strip().split()
323
+ w, h = int(parts[0]), int(parts[1])
324
  except:
325
+ w, h = 1920, 1080
326
+ res2 = run_command("xdotool getmouselocation", timeout=5)
327
+ try:
328
+ import re
329
+ mx = int(re.search(r"x:(\d+)", res2["stdout"]).group(1))
330
+ my = int(re.search(r"y:(\d+)", res2["stdout"]).group(1))
331
+ except:
332
+ mx, my = 0, 0
333
+ await send({"type": "screen_info", "width": w, "height": h,
334
+ "mouse_x": mx, "mouse_y": my})
335
 
336
  else:
337
+ await send({"type": "error", "msg": f"Unknown action: {action}"})
338
 
339
 
340
  # ─── WebSocket Endpoint ──────────────────────────────
 
343
  async def websocket_endpoint(ws: WebSocket):
344
  await ws.accept()
345
  active_connections.append(ws)
346
+ # Send welcome + first real screenshot
347
+ res = run_command("xdotool getdisplaygeometry", timeout=5)
348
  try:
349
+ parts = res["stdout"].strip().split()
350
+ w, h = int(parts[0]), int(parts[1])
351
  except:
352
  w, h = 1920, 1080
353
  await ws.send_text(json.dumps({
354
  "type": "connected",
355
+ "screen_width": w, "screen_height": h,
356
+ "msg": "Z Computer Mode v2 β€” Connected"
 
357
  }))
358
+ # Send initial screenshot immediately
359
+ frame = capture_screen(scale=0.75, quality=72)
360
+ if frame:
361
+ await ws.send_text(json.dumps({
362
+ "type": "screenshot", "data": frame,
363
+ "ts": int(time.time() * 1000), "label": "Initial screen"
364
+ }))
365
  try:
366
  while True:
367
  raw = await ws.receive_text()
 
370
  except WebSocketDisconnect:
371
  pass
372
  except Exception as e:
373
+ print(f"[ws] error: {e}")
374
  finally:
375
  if ws in active_connections:
376
  active_connections.remove(ws)
377
 
378
 
379
+ # ─── REST ────────────────────────────────────────────
380
 
381
  @app.get("/screenshot")
382
  async def rest_screenshot():
 
386
 
387
  @app.post("/terminal")
388
  async def rest_terminal(body: dict):
389
+ cmd = body.get("cmd", "")
390
+ res = run_command(cmd, timeout=body.get("timeout", 30))
391
+ return JSONResponse(res)
392
 
393
 
394
  @app.get("/health")
395
  async def health():
396
+ # Check if display is really working
397
+ res = run_command("xdotool getdisplaygeometry", timeout=4)
398
+ display_ok = res["returncode"] == 0
399
+ return {"status": "ok", "display": DISPLAY, "display_working": display_ok}
400
 
 
401
 
402
  if __name__ == "__main__":
403
  port = int(os.environ.get("PORT", 7860))
404
+ uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="info")