Files
Mark-LIV/ui.py
T
2026-07-05 18:01:31 +03:00

2467 lines
95 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import math
import os
import platform
import random
import subprocess
import sys
import threading
import time
from pathlib import Path
import psutil
if platform.system() == "Windows":
_WIN_HIDE: dict = {"creationflags": subprocess.CREATE_NO_WINDOW}
else:
_WIN_HIDE: dict = {}
from PyQt6.QtCore import (
QEasingCurve, QMimeData, QObject, QPointF, QRectF, QSize, Qt,
QTimer, QUrl, pyqtSignal,
)
from PyQt6.QtGui import (
QBrush, QColor, QDragEnterEvent, QDropEvent, QFont, QFontDatabase,
QKeySequence, QLinearGradient, QPainter, QPainterPath, QPen, QPixmap,
QRadialGradient, QShortcut,
)
from PyQt6.QtWidgets import (
QApplication, QFileDialog, QFrame, QHBoxLayout, QLabel, QLineEdit,
QMainWindow, QPushButton, QScrollArea, QSizePolicy, QSplitter,
QStackedWidget, QTextEdit, QVBoxLayout, QWidget, QProgressBar,
)
def _base_dir() -> Path:
if getattr(sys, "frozen", False):
return Path(sys.executable).parent
return Path(__file__).resolve().parent
BASE_DIR = _base_dir()
CONFIG_DIR = BASE_DIR / "config"
API_FILE = CONFIG_DIR / "api_keys.json"
_DEFAULT_W, _DEFAULT_H = 980, 700
_MIN_W, _MIN_H = 820, 580
_LEFT_W = 148
_RIGHT_W = 340
_OS = platform.system() # "Windows" | "Darwin" | "Linux"
class C:
BG = "#00060a"
PANEL = "#010d14"
PANEL2 = "#010f18"
BORDER = "#0d3347"
BORDER_B = "#1a5c7a"
BORDER_A = "#0f4060"
PRI = "#00d4ff"
PRI_DIM = "#007a99"
PRI_GHO = "#001f2e"
ACC = "#ff6b00"
ACC2 = "#ffcc00"
GREEN = "#00ff88"
GREEN_D = "#00aa55"
RED = "#ff3355"
MUTED_C = "#ff3366"
TEXT = "#8ffcff"
TEXT_DIM = "#3a8a9a"
TEXT_MED = "#5ab8cc"
WHITE = "#d8f8ff"
DARK = "#000d14"
BAR_BG = "#011520"
def qcol(h: str, a: int = 255) -> QColor:
c = QColor(h); c.setAlpha(a); return c
# ── Windows GPU via NVML DLL (no subprocess, no console window) ──────────────
_nvml_lib: object = None # cached ctypes DLL
_nvml_ok: object = None # None=untested, True=works, False=unavailable
def _nvml_gpu_windows() -> float:
"""Return NVIDIA GPU utilisation % using nvml.dll directly — zero subprocess."""
global _nvml_lib, _nvml_ok
if _nvml_ok is False:
return -1.0
try:
import ctypes
class _Util(ctypes.Structure):
_fields_ = [("gpu", ctypes.c_uint), ("memory", ctypes.c_uint)]
if _nvml_lib is None:
for dll_name in ("nvml", r"C:\Windows\System32\nvml.dll"):
try:
lib = ctypes.WinDLL(dll_name)
lib.nvmlInit_v2()
_nvml_lib = lib
break
except Exception:
continue
if _nvml_lib is None:
import pynvml # type: ignore
pynvml.nvmlInit()
h = pynvml.nvmlDeviceGetHandleByIndex(0)
_nvml_ok = True
return float(pynvml.nvmlDeviceGetUtilizationRates(h).gpu)
dev = ctypes.c_void_p()
_nvml_lib.nvmlDeviceGetHandleByIndex_v2(0, ctypes.byref(dev))
util = _Util()
_nvml_lib.nvmlDeviceGetUtilizationRates(dev, ctypes.byref(util))
_nvml_ok = True
return float(util.gpu)
except Exception:
_nvml_ok = False
return -1.0
class _SysMetrics:
def __init__(self):
self.cpu = 0.0
self.mem = 0.0
self.net = 0.0
self.gpu = -1.0
self.tmp = -1.0
self._lock = threading.Lock()
self._last_net = psutil.net_io_counters()
self._last_net_t = time.time()
self._running = True
t = threading.Thread(target=self._loop, daemon=True)
t.start()
def _loop(self):
while self._running:
try:
self._update()
except Exception:
pass
time.sleep(1.5)
def _update(self):
cpu = psutil.cpu_percent(interval=None)
mem = psutil.virtual_memory().percent
nc = psutil.net_io_counters()
now = time.time()
dt = now - self._last_net_t
if dt > 0:
sent = (nc.bytes_sent - self._last_net.bytes_sent) / dt
recv = (nc.bytes_recv - self._last_net.bytes_recv) / dt
net = (sent + recv) / (1024 * 1024)
else:
net = 0.0
self._last_net = nc
self._last_net_t = now
gpu = self._get_gpu()
tmp = self._get_temp()
with self._lock:
self.cpu = cpu
self.mem = mem
self.net = net
self.gpu = gpu
self.tmp = tmp
def _get_gpu(self) -> float:
# pynvml — subprocess-free, works on all platforms if installed
try:
import pynvml # type: ignore
pynvml.nvmlInit()
h = pynvml.nvmlDeviceGetHandleByIndex(0)
return float(pynvml.nvmlDeviceGetUtilizationRates(h).gpu)
except Exception:
pass
# Windows: nvml.dll via ctypes (already cached in _nvml_gpu_windows)
if _OS == "Windows":
return _nvml_gpu_windows()
# Linux / macOS: libnvidia-ml shared lib via ctypes
try:
import ctypes
_lib = "libnvidia-ml.so.1" if _OS == "Linux" else "libnvidia-ml.dylib"
class _Util(ctypes.Structure):
_fields_ = [("gpu", ctypes.c_uint), ("memory", ctypes.c_uint)]
nv = ctypes.CDLL(_lib)
nv.nvmlInit_v2()
dev = ctypes.c_void_p()
nv.nvmlDeviceGetHandleByIndex_v2(0, ctypes.byref(dev))
u = _Util()
nv.nvmlDeviceGetUtilizationRates(dev, ctypes.byref(u))
return float(u.gpu)
except Exception:
pass
return -1.0 # N/A — zero subprocess on all platforms
def _get_temp(self) -> float:
# psutil — works on Linux; occasionally Windows with driver support
try:
temps = psutil.sensors_temperatures()
for name in ["coretemp", "k10temp", "cpu_thermal", "acpitz",
"cpu-thermal", "zenpower", "it8688"]:
if name in temps and temps[name]:
return temps[name][0].current
for entries in temps.values():
if entries:
return entries[0].current
except Exception:
pass
# Windows: wmi module (pure Python COM, zero subprocess)
if _OS == "Windows":
try:
import wmi # type: ignore
w = wmi.WMI(namespace="root/wmi")
tz = w.MSAcpi_ThermalZoneTemperature()
if tz:
return (tz[0].CurrentTemperature / 10.0) - 273.15
except Exception:
pass
return -1.0 # N/A — zero subprocess on all platforms
def snapshot(self) -> dict:
with self._lock:
return {
"cpu": self.cpu,
"mem": self.mem,
"net": self.net,
"gpu": self.gpu,
"tmp": self.tmp,
}
_metrics = _SysMetrics()
class HudCanvas(QWidget):
def __init__(self, face_path: str, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent)
self.setMinimumSize(300, 300)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.muted = False
self.speaking = False
self.state = "INITIALISING"
self._tick = 0
self._scale = 1.0
self._tgt_scale = 1.0
self._halo = 55.0
self._tgt_halo = 55.0
self._last_t = time.time()
self._scan = 0.0
self._scan2 = 180.0
self._rings = [0.0, 120.0, 240.0]
self._pulses: list[float] = [0.0, 50.0, 100.0]
self._blink = True
self._blink_tick = 0
self._particles: list[list[float]] = []
self._face_px: QPixmap | None = None
self._load_face(face_path)
self._tmr = QTimer(self)
self._tmr.timeout.connect(self._step)
self._tmr.start(16)
def _load_face(self, path: str):
try:
from PIL import Image, ImageDraw
import io
img = Image.open(path).convert("RGBA")
sz = min(img.size)
img = img.resize((sz, sz), Image.LANCZOS)
mk = Image.new("L", (sz, sz), 0)
ImageDraw.Draw(mk).ellipse((2, 2, sz - 2, sz - 2), fill=255)
img.putalpha(mk)
buf = io.BytesIO()
img.save(buf, format="PNG")
px = QPixmap(); px.loadFromData(buf.getvalue())
self._face_px = px
except Exception:
self._face_px = None
def _step(self):
self._tick += 1
now = time.time()
if now - self._last_t > (0.12 if self.speaking else 0.5):
if self.speaking:
self._tgt_scale = random.uniform(1.06, 1.14)
self._tgt_halo = random.uniform(145, 190)
elif self.muted:
self._tgt_scale = random.uniform(0.998, 1.002)
self._tgt_halo = random.uniform(15, 28)
else:
self._tgt_scale = random.uniform(1.001, 1.008)
self._tgt_halo = random.uniform(48, 68)
self._last_t = now
sp = 0.38 if self.speaking else 0.15
self._scale += (self._tgt_scale - self._scale) * sp
self._halo += (self._tgt_halo - self._halo) * sp
speeds = [1.3, -0.9, 2.0] if self.speaking else [0.55, -0.35, 0.9]
for i, spd in enumerate(speeds):
self._rings[i] = (self._rings[i] + spd) % 360
self._scan = (self._scan + (3.0 if self.speaking else 1.3)) % 360
self._scan2 = (self._scan2 + (-2.0 if self.speaking else -0.75)) % 360
fw = min(self.width(), self.height())
lim = fw * 0.74
spd = 4.2 if self.speaking else 2.0
self._pulses = [r + spd for r in self._pulses if r + spd < lim]
if len(self._pulses) < 3 and random.random() < (0.07 if self.speaking else 0.025):
self._pulses.append(0.0)
if self.speaking and random.random() < 0.28:
cx, cy = self.width() / 2, self.height() / 2
ang = random.uniform(0, 2 * math.pi)
r_s = fw * 0.28
self._particles.append([
cx + math.cos(ang) * r_s, cy + math.sin(ang) * r_s,
math.cos(ang) * random.uniform(0.9, 2.4),
math.sin(ang) * random.uniform(0.9, 2.4) - 0.4, 1.0,
])
self._particles = [
[p[0]+p[2], p[1]+p[3], p[2]*0.97, p[3]*0.97, p[4]-0.028]
for p in self._particles if p[4] > 0
]
self._blink_tick += 1
if self._blink_tick >= 38:
self._blink = not self._blink
self._blink_tick = 0
self.update()
def paintEvent(self, _):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.fillRect(self.rect(), qcol(C.BG))
W, H = self.width(), self.height()
cx, cy = W / 2, H / 2
fw = min(W, H)
# grid dots
p.setPen(QPen(qcol(C.PRI_GHO), 1))
for x in range(0, W, 48):
for y in range(0, H, 48):
p.drawPoint(x, y)
r_face = fw * 0.31
# halo glow
for i in range(10):
r = r_face * (1.8 - i * 0.08)
frc = 1.0 - i / 10
a = max(0, min(255, int(self._halo * 0.085 * frc)))
col = qcol(C.MUTED_C if self.muted else C.PRI, a)
p.setPen(QPen(col, 1.5)); p.setBrush(Qt.BrushStyle.NoBrush)
p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
# pulse rings
for pr in self._pulses:
a = max(0, int(230 * (1.0 - pr / (fw * 0.74))))
col = qcol(C.MUTED_C if self.muted else C.PRI, a)
p.setPen(QPen(col, 1.5)); p.setBrush(Qt.BrushStyle.NoBrush)
p.drawEllipse(QRectF(cx - pr, cy - pr, pr * 2, pr * 2))
# spinning arc rings
for idx, (r_frac, w_r, arc_l, gap) in enumerate(
[(0.48, 3, 115, 78), (0.40, 2, 78, 55), (0.32, 1, 56, 40)]
):
ring_r = fw * r_frac
base = self._rings[idx]
a_val = max(0, min(255, int(self._halo * (1.0 - idx * 0.18))))
col = qcol(C.MUTED_C if self.muted else C.PRI, a_val)
p.setPen(QPen(col, w_r)); p.setBrush(Qt.BrushStyle.NoBrush)
angle = base
rect = QRectF(cx - ring_r, cy - ring_r, ring_r * 2, ring_r * 2)
while angle < base + 360:
p.drawArc(rect, int(angle * 16), int(arc_l * 16))
angle += arc_l + gap
# scanners
sr = fw * 0.50
sa = min(255, int(self._halo * 1.5))
ex = 75 if self.speaking else 44
p.setPen(QPen(qcol(C.MUTED_C if self.muted else C.PRI, sa), 2.5))
p.setBrush(Qt.BrushStyle.NoBrush)
srect = QRectF(cx - sr, cy - sr, sr * 2, sr * 2)
p.drawArc(srect, int(self._scan * 16), int(ex * 16))
p.setPen(QPen(qcol(C.ACC, sa // 2), 1.5))
p.drawArc(srect, int(self._scan2 * 16), int(ex * 16))
# tick marks
t_out, t_in = fw * 0.497, fw * 0.474
p.setPen(QPen(qcol(C.PRI, 140), 1))
for deg in range(0, 360, 10):
rad = math.radians(deg)
inn = t_in if deg % 30 == 0 else t_in + 6
p.drawLine(
QPointF(cx + t_out * math.cos(rad), cy - t_out * math.sin(rad)),
QPointF(cx + inn * math.cos(rad), cy - inn * math.sin(rad)),
)
# crosshair
ch_r, gap_h = fw * 0.51, fw * 0.16
p.setPen(QPen(qcol(C.PRI, int(self._halo * 0.5)), 1))
p.drawLine(QPointF(cx - ch_r, cy), QPointF(cx - gap_h, cy))
p.drawLine(QPointF(cx + gap_h, cy), QPointF(cx + ch_r, cy))
p.drawLine(QPointF(cx, cy - ch_r), QPointF(cx, cy - gap_h))
p.drawLine(QPointF(cx, cy + gap_h), QPointF(cx, cy + ch_r))
# corner brackets
bl = 24
bc = qcol(C.PRI, 210)
hl, hr = cx - fw // 2, cx + fw // 2
ht, hb = cy - fw // 2, cy + fw // 2
p.setPen(QPen(bc, 2))
for bx, by, dx, dy in [(hl,ht,1,1),(hr,ht,-1,1),(hl,hb,1,-1),(hr,hb,-1,-1)]:
p.drawLine(QPointF(bx, by), QPointF(bx + dx * bl, by))
p.drawLine(QPointF(bx, by), QPointF(bx, by + dy * bl))
# face
if self._face_px:
fsz = int(fw * 0.62 * self._scale)
scaled = self._face_px.scaled(
fsz, fsz,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
p.drawPixmap(int(cx - fsz / 2), int(cy - fsz / 2), scaled)
else:
orb_r = int(fw * 0.27 * self._scale)
oc = (200, 0, 50) if self.muted else (0, 60, 110)
for i in range(8, 0, -1):
r2 = int(orb_r * i / 8)
frc = i / 8
a = max(0, min(255, int(self._halo * 1.1 * frc)))
p.setBrush(QBrush(QColor(int(oc[0]*frc), int(oc[1]*frc), int(oc[2]*frc), a)))
p.setPen(Qt.PenStyle.NoPen)
p.drawEllipse(QRectF(cx - r2, cy - r2, r2 * 2, r2 * 2))
p.setPen(QPen(qcol(C.PRI, min(255, int(self._halo * 2))), 1))
p.setFont(QFont("Courier New", 13, QFont.Weight.Bold))
p.drawText(QRectF(cx - 80, cy - 14, 160, 28),
Qt.AlignmentFlag.AlignCenter, "J.A.R.V.I.S")
# particles
for pt in self._particles:
a = max(0, min(255, int(pt[4] * 255)))
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QBrush(qcol(C.PRI, a)))
p.drawEllipse(QPointF(pt[0], pt[1]), 2.5, 2.5)
# status text
sy = cy + fw * 0.40
if self.muted:
txt, col = "⊘ MUTED", qcol(C.MUTED_C)
elif self.speaking:
txt, col = "● SPEAKING", qcol(C.ACC)
elif self.state == "THINKING":
sym = "◈" if self._blink else "◇"
txt, col = f"{sym} THINKING", qcol(C.ACC2)
elif self.state == "PROCESSING":
sym = "▷" if self._blink else "▶"
txt, col = f"{sym} PROCESSING", qcol(C.ACC2)
elif self.state == "LISTENING":
sym = "●" if self._blink else "○"
txt, col = f"{sym} LISTENING", qcol(C.GREEN)
else:
sym = "●" if self._blink else "○"
txt, col = f"{sym} {self.state}", qcol(C.PRI)
p.setPen(QPen(col, 1))
p.setFont(QFont("Courier New", 11, QFont.Weight.Bold))
p.drawText(QRectF(0, sy, W, 26), Qt.AlignmentFlag.AlignCenter, txt)
# waveform
wy = sy + 30
N, bw = 36, 8
wx0 = (W - N * bw) / 2
for i in range(N):
if self.muted:
hgt, cl = 2, qcol(C.MUTED_C)
elif self.speaking:
hgt = random.randint(3, 20)
cl = qcol(C.PRI) if hgt > 12 else qcol(C.PRI_DIM)
else:
hgt = int(3 + 2 * math.sin(self._tick * 0.09 + i * 0.6))
cl = qcol(C.BORDER_B)
p.fillRect(QRectF(wx0 + i * bw, wy + 20 - hgt, bw - 1, hgt), cl)
class MetricBar(QWidget):
def __init__(self, label: str, color: str = C.PRI, parent=None):
super().__init__(parent)
self._label = label
self._color = color
self._value = 0.0 # 0–100
self._text = "--"
self.setFixedHeight(38)
self.setMinimumWidth(80)
def set_value(self, pct: float, text: str):
self._value = max(0.0, min(100.0, pct))
self._text = text
self.update()
def paintEvent(self, _):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
W, H = self.width(), self.height()
p.setBrush(QBrush(qcol(C.PANEL2)))
p.setPen(QPen(qcol(C.BORDER_A), 1))
p.drawRoundedRect(QRectF(1, 1, W - 2, H - 2), 4, 4)
bar_h = 4
bar_y = H - bar_h - 5
bar_w = W - 12
bar_x = 6
fill_w = int(bar_w * self._value / 100)
p.setBrush(QBrush(qcol(C.BAR_BG)))
p.setPen(Qt.PenStyle.NoPen)
p.drawRoundedRect(QRectF(bar_x, bar_y, bar_w, bar_h), 2, 2)
if self._value > 85:
bar_col = qcol(C.RED)
elif self._value > 65:
bar_col = qcol(C.ACC)
else:
bar_col = qcol(self._color)
if fill_w > 0:
p.setBrush(QBrush(bar_col))
p.drawRoundedRect(QRectF(bar_x, bar_y, fill_w, bar_h), 2, 2)
p.setFont(QFont("Courier New", 7, QFont.Weight.Bold))
p.setPen(QPen(qcol(C.TEXT_DIM), 1))
p.drawText(QRectF(8, 5, 50, 14), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self._label)
p.setFont(QFont("Courier New", 9, QFont.Weight.Bold))
p.setPen(QPen(bar_col if self._text != "--" else qcol(C.TEXT_DIM), 1))
p.drawText(QRectF(0, 4, W - 6, 16), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, self._text)
class LogWidget(QTextEdit):
_sig = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setFont(QFont("Courier New", 9))
self.setStyleSheet(f"""
QTextEdit {{
background: {C.PANEL};
color: {C.TEXT};
border: 1px solid {C.BORDER};
border-radius: 4px;
padding: 6px;
selection-background-color: {C.PRI_GHO};
}}
QScrollBar:vertical {{
background: {C.BG};
width: 8px;
border: none;
}}
QScrollBar::handle:vertical {{
background: {C.BORDER_B};
border-radius: 4px;
min-height: 20px;
}}
""")
self._queue: list[str] = []
self._typing = False
self._text = ""
self._pos = 0
self._tag = "sys"
self._tmr = QTimer(self)
self._tmr.timeout.connect(self._step)
self._sig.connect(self._enqueue)
def append_log(self, text: str):
self._sig.emit(text)
def _enqueue(self, text: str):
self._queue.append(text)
if not self._typing:
self._next()
def _next(self):
if not self._queue:
self._typing = False
return
self._typing = True
self._text = self._queue.pop(0)
self._pos = 0
tl = self._text.lower()
if tl.startswith("you:"): self._tag = "you"
elif tl.startswith("jarvis:"): self._tag = "ai"
elif tl.startswith("file:"): self._tag = "file"
elif "err" in tl: self._tag = "err"
else: self._tag = "sys"
self._tmr.start(6)
def _step(self):
if self._pos < len(self._text):
ch = self._text[self._pos]
cur = self.textCursor()
fmt = cur.charFormat()
col = {
"you": qcol(C.WHITE),
"ai": qcol(C.PRI),
"err": qcol(C.RED),
"file": qcol(C.GREEN),
"sys": qcol(C.ACC2),
}.get(self._tag, qcol(C.TEXT))
fmt.setForeground(QBrush(col))
cur.movePosition(cur.MoveOperation.End)
cur.insertText(ch, fmt)
self.setTextCursor(cur)
self.ensureCursorVisible()
self._pos += 1
else:
self._tmr.stop()
cur = self.textCursor()
cur.movePosition(cur.MoveOperation.End)
cur.insertText("\n")
self.setTextCursor(cur)
self.ensureCursorVisible()
QTimer.singleShot(20, self._next)
_FILE_ICONS = {
"image": ("🖼", "#00d4ff"), "video": ("🎬", "#ff6b00"),
"audio": ("🎵", "#cc44ff"), "pdf": ("📄", "#ff4444"),
"word": ("📝", "#4488ff"), "excel": ("📊", "#44bb44"),
"code": ("💻", "#ffcc00"), "archive": ("📦", "#ff8844"),
"pptx": ("📊", "#ff6622"), "text": ("📃", "#aaaaaa"),
"data": ("🔧", "#88ddff"), "unknown": ("📎", "#888888"),
}
_EXT_TO_CAT = {
**dict.fromkeys(["jpg","jpeg","png","gif","webp","bmp","tiff","svg","ico"], "image"),
**dict.fromkeys(["mp4","avi","mov","mkv","wmv","flv","webm","m4v"], "video"),
**dict.fromkeys(["mp3","wav","ogg","m4a","aac","flac","wma","opus"], "audio"),
**dict.fromkeys(["pdf"], "pdf"),
**dict.fromkeys(["doc","docx"], "word"),
**dict.fromkeys(["xls","xlsx","ods"], "excel"),
**dict.fromkeys(["ppt","pptx"], "pptx"),
**dict.fromkeys(["py","js","ts","jsx","tsx","html","css","java","c","cpp",
"cs","go","rs","rb","php","swift","kt","sh","sql","lua"], "code"),
**dict.fromkeys(["zip","rar","tar","gz","7z","bz2","xz"], "archive"),
**dict.fromkeys(["txt","md","rst","log"], "text"),
**dict.fromkeys(["csv","tsv","json","xml"], "data"),
}
def _file_category(path: Path) -> str:
return _EXT_TO_CAT.get(path.suffix.lower().lstrip("."), "unknown")
def _fmt_size(size: int) -> str:
if size < 1024: return f"{size} B"
elif size < 1024**2: return f"{size/1024:.1f} KB"
elif size < 1024**3: return f"{size/1024**2:.1f} MB"
else: return f"{size/1024**3:.1f} GB"
class FileDropZone(QWidget):
file_selected = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setFixedHeight(100)
self._current_file: str | None = None
self._hovering = False
self._drag_over = False
self._dash_offset = 0.0
self._anim_tmr = QTimer(self)
self._anim_tmr.timeout.connect(self._animate)
self._anim_tmr.start(40)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self._canvas = _DropCanvas(self)
layout.addWidget(self._canvas)
def _animate(self):
self._dash_offset = (self._dash_offset + 0.8) % 20
self._canvas.update()
def dragEnterEvent(self, e: QDragEnterEvent):
if e.mimeData().hasUrls():
e.acceptProposedAction()
self._drag_over = True; self._canvas.update()
def dragLeaveEvent(self, e):
self._drag_over = False; self._canvas.update()
def dropEvent(self, e: QDropEvent):
self._drag_over = False
urls = e.mimeData().urls()
if urls:
path = urls[0].toLocalFile()
if Path(path).is_file():
self._set_file(path)
self._canvas.update()
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.LeftButton:
self._browse()
def enterEvent(self, e):
self._hovering = True; self._canvas.update()
def leaveEvent(self, e):
self._hovering = False; self._canvas.update()
def current_file(self) -> str | None:
return self._current_file
def clear_file(self):
self._current_file = None; self._canvas.update()
def _browse(self):
path, _ = QFileDialog.getOpenFileName(
self, "Select a file for JARVIS", str(Path.home()),
"All Files (*.*);;"
"Images (*.jpg *.jpeg *.png *.gif *.webp *.bmp *.svg);;"
"Documents (*.pdf *.docx *.txt *.md *.pptx);;"
"Data (*.csv *.xlsx *.json *.xml);;"
"Code (*.py *.js *.ts *.html *.css *.java *.cpp *.go);;"
"Audio (*.mp3 *.wav *.ogg *.m4a *.aac *.flac);;"
"Video (*.mp4 *.avi *.mov *.mkv *.wmv *.webm);;"
"Archives (*.zip *.rar *.tar *.gz *.7z)",
)
if path:
self._set_file(path)
def _set_file(self, path: str):
self._current_file = path
self._canvas.update()
self.file_selected.emit(path)
class _DropCanvas(QWidget):
def __init__(self, zone: FileDropZone):
super().__init__(zone)
self._z = zone
def paintEvent(self, _):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
z = self._z
W, H = self.width(), self.height()
pad = 6
rect = QRectF(pad, pad, W - pad * 2, H - pad * 2)
bg_col = qcol("#001a24" if z._drag_over else ("#001218" if z._hovering else C.PANEL))
p.setBrush(QBrush(bg_col)); p.setPen(Qt.PenStyle.NoPen)
p.drawRoundedRect(rect, 6, 6)
if z._current_file: border_col = qcol(C.GREEN, 200)
elif z._drag_over: border_col = qcol(C.PRI, 230)
elif z._hovering: border_col = qcol(C.BORDER_B, 200)
else: border_col = qcol(C.BORDER, 160)
pen = QPen(border_col, 1.5, Qt.PenStyle.DashLine)
pen.setDashOffset(z._dash_offset)
p.setPen(pen); p.setBrush(Qt.BrushStyle.NoBrush)
p.drawRoundedRect(rect, 6, 6)
if z._current_file: self._paint_file(p, W, H)
elif z._drag_over: self._paint_drag_over(p, W, H)
else: self._paint_idle(p, W, H, z._hovering)
def _paint_idle(self, p, W, H, hover):
cx, cy = W / 2, H / 2
col = qcol(C.PRI_DIM if not hover else C.PRI)
p.setPen(QPen(col, 2)); p.setBrush(Qt.BrushStyle.NoBrush)
p.drawLine(QPointF(cx, cy - 14), QPointF(cx, cy + 4))
p.drawLine(QPointF(cx - 8, cy - 6), QPointF(cx, cy - 14))
p.drawLine(QPointF(cx + 8, cy - 6), QPointF(cx, cy - 14))
p.drawLine(QPointF(cx - 14, cy + 4), QPointF(cx + 14, cy + 4))
p.setFont(QFont("Courier New", 8))
p.setPen(QPen(qcol(C.PRI_DIM if not hover else C.TEXT), 1))
p.drawText(QRectF(0, cy + 8, W, 16), Qt.AlignmentFlag.AlignCenter,
"Drop file here or Click to Browse")
p.setFont(QFont("Courier New", 7))
p.setPen(QPen(qcol("#1a4a5a"), 1))
p.drawText(QRectF(0, cy + 24, W, 14), Qt.AlignmentFlag.AlignCenter,
"Images · Video · Audio · PDF · Docs · Code · Data")
def _paint_drag_over(self, p, W, H):
cx, cy = W / 2, H / 2
p.setFont(QFont("Courier New", 20))
p.setPen(QPen(qcol(C.PRI), 1))
p.drawText(QRectF(0, cy - 24, W, 32), Qt.AlignmentFlag.AlignCenter, "⬇")
p.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
p.setPen(QPen(qcol(C.PRI), 1))
p.drawText(QRectF(0, cy + 12, W, 16), Qt.AlignmentFlag.AlignCenter, "Release to load")
def _paint_file(self, p, W, H):
path = Path(self._z._current_file)
cat = _file_category(path)
icon, icon_col = _FILE_ICONS.get(cat, _FILE_ICONS["unknown"])
size_str = _fmt_size(path.stat().st_size)
ext_str = path.suffix.upper().lstrip(".") or "FILE"
block_x, block_w = 10, 60
p.setFont(QFont("Segoe UI Emoji", 22) if _OS == "Windows" else QFont("Arial", 22))
p.setPen(QPen(qcol(icon_col), 1))
p.drawText(QRectF(block_x, 0, block_w, H), Qt.AlignmentFlag.AlignCenter, icon)
tx = block_x + block_w + 6
tw = W - tx - 38
p.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
p.setPen(QPen(qcol(C.WHITE), 1))
name = path.name if len(path.name) <= 34 else path.name[:31] + "..."
p.drawText(QRectF(tx, H * 0.18, tw, 16),
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, name)
p.setFont(QFont("Courier New", 7))
p.setPen(QPen(qcol(C.TEXT_DIM), 1))
p.drawText(QRectF(tx, H * 0.18 + 18, tw, 14),
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{ext_str} · {size_str}")
p.setFont(QFont("Courier New", 6))
p.setPen(QPen(qcol("#1e5c6a"), 1))
par = str(path.parent)
if len(par) > 42: par = "…" + par[-41:]
p.drawText(QRectF(tx, H * 0.18 + 34, tw, 12),
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, par)
p.setFont(QFont("Courier New", 9, QFont.Weight.Bold))
p.setPen(QPen(qcol(C.RED, 180), 1))
p.drawText(QRectF(W - 34, 0, 28, H), Qt.AlignmentFlag.AlignCenter, "✕")
def mousePressEvent(self, e):
z = self._z
if z._current_file and e.pos().x() > self.width() - 34:
z.clear_file()
else:
z.mousePressEvent(e)
class _CameraPreview(QWidget):
"""Floating overlay that briefly shows what the camera captured."""
_W, _H = 244, 188
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setStyleSheet(f"""
_CameraPreview {{
background: rgba(0, 6, 10, 242);
border: 1px solid {C.PRI};
border-radius: 6px;
}}
""")
self.setFixedWidth(self._W)
lay = QVBoxLayout(self)
lay.setContentsMargins(6, 5, 6, 6)
lay.setSpacing(4)
hdr = QHBoxLayout()
title = QLabel("◈ VISUAL INPUT")
title.setFont(QFont("Courier New", 7, QFont.Weight.Bold))
title.setStyleSheet(f"color: {C.PRI}; background: transparent;")
hdr.addWidget(title)
hdr.addStretch()
close_btn = QPushButton("✕")
close_btn.setFixedSize(16, 16)
close_btn.setFont(QFont("Courier New", 8))
close_btn.setStyleSheet(
f"color: {C.TEXT_DIM}; background: transparent; border: none;"
)
close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
close_btn.clicked.connect(self.hide)
hdr.addWidget(close_btn)
lay.addLayout(hdr)
self._img_lbl = QLabel()
self._img_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._img_lbl.setStyleSheet("background: transparent;")
lay.addWidget(self._img_lbl)
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.timeout.connect(self.hide)
self.hide()
def show_frame(self, img_bytes: bytes) -> None:
px = QPixmap()
px.loadFromData(img_bytes)
if not px.isNull():
max_w = self._W - 12
scaled = px.scaled(
max_w, 160,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self._img_lbl.setPixmap(scaled)
self._img_lbl.setFixedSize(scaled.width(), scaled.height())
self.adjustSize()
self.show()
self.raise_()
self._timer.start(6_000) # auto-dismiss after 6 s
class SetupOverlay(QWidget):
done = pyqtSignal(str, str)
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setStyleSheet(f"""
SetupOverlay {{
background: rgba(0, 6, 10, 245);
border: 1px solid {C.BORDER_B};
border-radius: 6px;
}}
""")
detected = {"darwin": "mac", "windows": "windows"}.get(
_OS.lower(), "linux"
)
self._sel_os = detected
layout = QVBoxLayout(self)
layout.setContentsMargins(30, 22, 30, 22)
layout.setSpacing(8)
def _lbl(txt, font_size=9, bold=False, color=C.PRI,
align=Qt.AlignmentFlag.AlignCenter):
w = QLabel(txt)
w.setAlignment(align)
w.setFont(QFont("Courier New", font_size,
QFont.Weight.Bold if bold else QFont.Weight.Normal))
w.setStyleSheet(f"color: {color}; background: transparent;")
return w
layout.addWidget(_lbl("◈ INITIALISATION REQUIRED", 13, True))
layout.addWidget(_lbl("Configure J.A.R.V.I.S. before first boot.", 9, color=C.PRI_DIM))
layout.addSpacing(6)
sep = QFrame(); sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet(f"color: {C.BORDER};"); layout.addWidget(sep)
layout.addSpacing(4)
layout.addWidget(_lbl("GEMINI API KEY", 8, color=C.TEXT_DIM,
align=Qt.AlignmentFlag.AlignLeft))
self._key_input = QLineEdit()
self._key_input.setEchoMode(QLineEdit.EchoMode.Password)
self._key_input.setPlaceholderText("AIza…")
self._key_input.setFont(QFont("Courier New", 10))
self._key_input.setFixedHeight(32)
self._key_input.setStyleSheet(f"""
QLineEdit {{
background: #000d12; color: {C.TEXT};
border: 1px solid {C.BORDER}; border-radius: 3px; padding: 4px 8px;
}}
QLineEdit:focus {{ border: 1px solid {C.PRI}; }}
""")
layout.addWidget(self._key_input)
layout.addSpacing(12)
sep2 = QFrame(); sep2.setFrameShape(QFrame.Shape.HLine)
sep2.setStyleSheet(f"color: {C.BORDER};"); layout.addWidget(sep2)
layout.addSpacing(4)
layout.addWidget(_lbl("OPERATING SYSTEM", 8, color=C.TEXT_DIM,
align=Qt.AlignmentFlag.AlignLeft))
det_name = {"windows": "Windows", "mac": "macOS", "linux": "Linux"}[detected]
layout.addWidget(_lbl(f"Auto-detected: {det_name}", 8, color=C.ACC2,
align=Qt.AlignmentFlag.AlignLeft))
os_row = QHBoxLayout(); os_row.setSpacing(6)
self._os_btns: dict[str, QPushButton] = {}
for key, label in [("windows","⊞ Windows"),("mac"," macOS"),("linux","🐧 Linux")]:
btn = QPushButton(label)
btn.setFont(QFont("Courier New", 9, QFont.Weight.Bold))
btn.setFixedHeight(32)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
btn.clicked.connect(lambda _, k=key: self._sel(k))
os_row.addWidget(btn)
self._os_btns[key] = btn
layout.addLayout(os_row)
self._sel(detected)
layout.addSpacing(12)
init_btn = QPushButton("▸ INITIALISE SYSTEMS")
init_btn.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
init_btn.setFixedHeight(36)
init_btn.setCursor(Qt.CursorShape.PointingHandCursor)
init_btn.setStyleSheet(f"""
QPushButton {{
background: transparent; color: {C.PRI};
border: 1px solid {C.PRI_DIM}; border-radius: 3px;
}}
QPushButton:hover {{
background: {C.PRI_GHO}; border: 1px solid {C.PRI};
}}
""")
init_btn.clicked.connect(self._submit)
layout.addWidget(init_btn)
def _sel(self, key: str):
self._sel_os = key
pal = {"windows":(C.PRI,"#001a22"),"mac":(C.ACC2,"#1a1400"),"linux":(C.GREEN,"#001a0d")}
for k, btn in self._os_btns.items():
if k == key:
fg, bg = pal[k]
btn.setStyleSheet(f"""
QPushButton {{
background: {fg}; color: {bg};
border: none; border-radius: 3px; font-weight: bold;
}}
""")
else:
btn.setStyleSheet(f"""
QPushButton {{
background: #000d12; color: {C.TEXT_DIM};
border: 1px solid {C.BORDER}; border-radius: 3px;
}}
QPushButton:hover {{ color: {C.TEXT}; border: 1px solid {C.BORDER_B}; }}
""")
def _submit(self):
key = self._key_input.text().strip()
if not key:
self._key_input.setStyleSheet(
self._key_input.styleSheet() +
f" QLineEdit {{ border: 1px solid {C.RED}; }}"
)
return
self.done.emit(key, self._sel_os)
class RemoteKeyOverlay(QWidget):
"""Floating overlay — QR code for instant phone pairing + manual key fallback."""
closed = pyqtSignal()
_OW, _OH = 400, 465
def __init__(self, url: str, key: str, auto_login_url: str = "",
manual_url: str = "", expiry_secs: int = 600, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setStyleSheet(f"""
RemoteKeyOverlay {{
background: rgba(0, 4, 12, 0.95);
border: 1px solid {C.BORDER_B};
border-radius: 14px;
}}
""")
self._expiry = time.time() + expiry_secs
self._on_new_key = None
self._auto_login_url = auto_login_url
self._manual_url = manual_url or url
lay = QVBoxLayout(self)
lay.setContentsMargins(24, 16, 24, 16)
lay.setSpacing(5)
def _lbl(txt, fs=9, bold=False, color=C.PRI,
align=Qt.AlignmentFlag.AlignCenter):
w = QLabel(txt)
w.setAlignment(align)
w.setFont(QFont("Courier New", fs,
QFont.Weight.Bold if bold else QFont.Weight.Normal))
w.setStyleSheet(f"color: {color}; background: transparent;")
w.setWordWrap(True)
return w
lay.addWidget(_lbl("◈ REMOTE ACCESS", 12, True))
sep = QFrame(); sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet(f"color: {C.BORDER}; margin: 1px 0;")
lay.addWidget(sep)
# ── QR code ───────────────────────────────────────────────────────────
self._qr_label = QLabel()
self._qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._qr_label.setFixedSize(176, 176)
self._qr_label.setStyleSheet(
"background: white; border-radius: 10px; padding: 4px;"
)
qr_row = QHBoxLayout()
qr_row.addStretch()
qr_row.addWidget(self._qr_label)
qr_row.addStretch()
lay.addLayout(qr_row)
self._update_qr(auto_login_url)
lay.addWidget(_lbl("Scan with phone camera to connect instantly", 8, color=C.TEXT_DIM))
sep2 = QFrame(); sep2.setFrameShape(QFrame.Shape.HLine)
sep2.setStyleSheet(f"color: {C.BORDER}; margin: 1px 0;")
lay.addWidget(sep2)
lay.addWidget(_lbl("Or enter manually:", 7, color=C.TEXT_DIM,
align=Qt.AlignmentFlag.AlignLeft))
self._url_lbl = QLabel(self._manual_url)
self._url_lbl.setFont(QFont("Courier New", 8))
self._url_lbl.setStyleSheet(f"color: {C.PRI_DIM}; background: transparent;")
self._url_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._url_lbl.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse)
lay.addWidget(self._url_lbl)
self._key_lbl = QLabel(key)
self._key_lbl.setFont(QFont("Courier New", 28, QFont.Weight.Bold))
self._key_lbl.setStyleSheet(f"""
color: {C.ACC};
background: {C.PANEL2};
border: 1px solid {C.BORDER_B};
border-radius: 8px;
padding: 6px 4px;
letter-spacing: 10px;
""")
self._key_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
lay.addWidget(self._key_lbl)
self._timer_lbl = QLabel()
self._timer_lbl.setFont(QFont("Courier New", 8))
self._timer_lbl.setStyleSheet(f"color: {C.TEXT_MED}; background: transparent;")
self._timer_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
lay.addWidget(self._timer_lbl)
btn_row = QHBoxLayout(); btn_row.setSpacing(8)
new_btn = QPushButton("NEW KEY")
new_btn.setFixedHeight(32)
new_btn.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
new_btn.setCursor(Qt.CursorShape.PointingHandCursor)
new_btn.setStyleSheet(f"""
QPushButton {{
background: {C.PANEL}; color: {C.PRI};
border: 1px solid {C.PRI_DIM}; border-radius: 5px;
}}
QPushButton:hover {{ background: {C.PRI_GHO}; border: 1px solid {C.PRI}; }}
""")
new_btn.clicked.connect(self._refresh_key)
btn_row.addWidget(new_btn)
close_btn = QPushButton("DISMISS")
close_btn.setFixedHeight(32)
close_btn.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
close_btn.setStyleSheet(f"""
QPushButton {{
background: transparent; color: {C.TEXT_MED};
border: 1px solid {C.BORDER}; border-radius: 5px;
}}
QPushButton:hover {{ color: {C.TEXT}; border: 1px solid {C.BORDER_B}; }}
""")
close_btn.clicked.connect(self._do_close)
btn_row.addWidget(close_btn)
lay.addLayout(btn_row)
self._ctimer = QTimer(self)
self._ctimer.timeout.connect(self._tick)
self._ctimer.start(1000)
self._tick()
def set_new_key_callback(self, fn) -> None:
self._on_new_key = fn
def _update_qr(self, url: str) -> None:
if not url:
self._qr_label.setText("—")
return
try:
import qrcode as _qrmod
from io import BytesIO
qr = _qrmod.QRCode(
box_size=5, border=2,
error_correction=_qrmod.constants.ERROR_CORRECT_M,
)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = BytesIO()
img.save(buf, format="PNG")
px = QPixmap()
px.loadFromData(buf.getvalue())
self._qr_label.setPixmap(
px.scaled(170, 170,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
)
except ImportError:
self._qr_label.setText("pip install\nqrcode[pil]")
self._qr_label.setFont(QFont("Courier New", 8))
self._qr_label.setStyleSheet(
"color: #888; background: white; border-radius: 10px; padding: 4px;"
)
except Exception:
self._qr_label.setText(url[:28])
self._qr_label.setFont(QFont("Courier New", 7))
self._qr_label.setStyleSheet(
f"color: {C.PRI}; background: white; border-radius: 10px; padding: 4px;"
)
def _tick(self):
remaining = max(0, int(self._expiry - time.time()))
m, s = divmod(remaining, 60)
self._timer_lbl.setText(f"Key expires in {m:02d}:{s:02d}")
if remaining == 0:
self._do_close()
def mark_connected(self) -> None:
"""Call from any thread when a phone successfully connects."""
self._ctimer.stop()
self._key_lbl.setText("CONNECTED")
self._key_lbl.setStyleSheet(f"""
color: {C.GREEN};
background: rgba(34,197,94,0.08);
border: 2px solid rgba(34,197,94,0.4);
border-radius: 8px;
padding: 6px 4px;
letter-spacing: 4px;
""")
self._qr_label.setText("✓")
self._qr_label.setFont(QFont("Courier New", 54, QFont.Weight.Bold))
self._qr_label.setStyleSheet(
"color: #00ff88; background: #001a0d; border-radius: 10px;"
)
self._timer_lbl.setText("Phone connected — JARVIS ready")
self._timer_lbl.setStyleSheet(f"color: {C.GREEN}; background: transparent;")
def _refresh_key(self):
if self._on_new_key:
result = self._on_new_key()
if result:
url = result[0]
key = result[1]
auto = result[2] if len(result) >= 3 else ""
manual = result[3] if len(result) >= 4 else url
self._manual_url = manual or url
self._url_lbl.setText(self._manual_url)
self._key_lbl.setText(key)
self._auto_login_url = auto
self._update_qr(auto or url)
self._expiry = time.time() + 600
self._key_lbl.setStyleSheet(f"""
color: {C.ACC};
background: {C.PANEL2};
border: 1px solid {C.BORDER_B};
border-radius: 8px;
padding: 6px 4px;
letter-spacing: 10px;
""")
self._timer_lbl.setStyleSheet(
f"color: {C.TEXT_MED}; background: transparent;"
)
self._ctimer.start(1000)
self._tick()
def _do_close(self):
self._ctimer.stop()
self.hide()
self.closed.emit()
class MainWindow(QMainWindow):
_log_sig = pyqtSignal(str)
_state_sig = pyqtSignal(str)
_content_sig = pyqtSignal(str, str) # (title, text) — thread-safe content display
_reconfig_sig = pyqtSignal() # trigger setup overlay from any thread
_camera_sig = pyqtSignal(bytes) # show camera frame preview (small overlay)
_cam_stream_sig = pyqtSignal(bool) # True=start live stream, False=stop
_cam_frame_sig = pyqtSignal(bytes) # live camera frame → HUD area
def __init__(self, face_path: str):
super().__init__()
self._face_path = face_path
self.setWindowTitle("J.A.R.V.I.S — MARK XLVIII")
self.setMinimumSize(_MIN_W, _MIN_H)
self.resize(_DEFAULT_W, _DEFAULT_H)
screen = QApplication.primaryScreen().availableGeometry()
self.move(
(screen.width() - _DEFAULT_W) // 2,
(screen.height() - _DEFAULT_H) // 2,
)
self.on_text_command = None
self.on_remote_clicked = None # callable: () -> (url, key) | None
self.on_interrupt = None # callable: () -> None — stop JARVIS mid-speech
self._muted = False
self._current_file: str | None = None
self._remote_overlay: RemoteKeyOverlay | None = None
central = QWidget()
central.setStyleSheet(f"background: {C.BG};")
self.setCentralWidget(central)
root = QVBoxLayout(central)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
root.addWidget(self._build_header())
body = QHBoxLayout()
body.setContentsMargins(0, 0, 0, 0)
body.setSpacing(0)
self._left_panel = self._build_left_panel()
body.addWidget(self._left_panel, stretch=0)
# Center column: HUD + resizable content panel via QSplitter
self.hud = HudCanvas(face_path)
self.hud.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self._content_panel = self._build_content_panel()
# Live camera container — replaces HUD when camera stream is active
_cam_cont = QWidget()
_cam_cont.setStyleSheet("background: #000308;")
_cam_v = QVBoxLayout(_cam_cont)
_cam_v.setContentsMargins(0, 0, 0, 0)
_cam_v.setSpacing(0)
_cam_hdr = QHBoxLayout()
_cam_hdr.setContentsMargins(8, 5, 8, 5)
_cam_title = QLabel("◈ CAMERA FEED")
_cam_title.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
_cam_title.setStyleSheet(f"color: {C.PRI}; background: transparent;")
_cam_hdr.addWidget(_cam_title)
_cam_hdr.addStretch()
_cam_x = QPushButton("✕ CLOSE")
_cam_x.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
_cam_x.setCursor(Qt.CursorShape.PointingHandCursor)
_cam_x.setStyleSheet(f"""
QPushButton {{
color: {C.TEXT_DIM}; background: transparent;
border: none; padding: 2px 6px;
}}
QPushButton:hover {{ color: {C.PRI}; }}
""")
_cam_x.clicked.connect(self.stop_camera_stream)
_cam_hdr.addWidget(_cam_x)
_cam_v.addLayout(_cam_hdr)
self._cam_live_lbl = QLabel()
self._cam_live_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._cam_live_lbl.setStyleSheet("background: transparent;")
self._cam_live_lbl.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
_cam_v.addWidget(self._cam_live_lbl, stretch=1)
# Stack: 0 = animated HUD, 1 = live camera
self._hud_cam_stack = QStackedWidget()
self._hud_cam_stack.addWidget(self.hud)
self._hud_cam_stack.addWidget(_cam_cont)
self._center_split = QSplitter(Qt.Orientation.Vertical)
self._center_split.setStyleSheet(f"""
QSplitter::handle {{
background: {C.BORDER};
height: 4px;
}}
QSplitter::handle:hover {{
background: {C.PRI_DIM};
}}
""")
self._center_split.addWidget(self._hud_cam_stack)
self._center_split.addWidget(self._content_panel)
self._center_split.setStretchFactor(0, 3)
self._center_split.setStretchFactor(1, 1)
self._center_split.setCollapsible(0, False)
body.addWidget(self._center_split, stretch=5)
self._right_panel = self._build_right_panel()
body.addWidget(self._right_panel, stretch=0)
root.addLayout(body, stretch=1)
root.addWidget(self._build_footer())
self._clock_tmr = QTimer(self)
self._clock_tmr.timeout.connect(self._tick_clock)
self._clock_tmr.start(1000)
self._tick_clock()
# Metrik güncelleme timer'ı
self._metric_tmr = QTimer(self)
self._metric_tmr.timeout.connect(self._update_metrics)
self._metric_tmr.start(2000)
self._update_metrics()
self._log_sig.connect(self._log.append_log)
self._state_sig.connect(self._apply_state)
self._content_sig.connect(self._show_content)
self._reconfig_sig.connect(self._show_setup)
self._camera_sig.connect(self._show_camera_frame)
self._cam_stream_sig.connect(self._on_cam_stream)
self._cam_frame_sig.connect(self._on_cam_frame)
self._cam_stop = threading.Event()
# Camera preview overlay (child of central widget, positioned in resizeEvent)
self._cam_preview = _CameraPreview(self.centralWidget())
self._overlay: SetupOverlay | None = None
self._ready = self._check_config()
if not self._ready:
self._show_setup()
sc_mute = QShortcut(QKeySequence("F4"), self)
sc_mute.activated.connect(self._toggle_mute)
sc_full = QShortcut(QKeySequence("F11"), self)
sc_full.activated.connect(self._toggle_fullscreen)
sc_intr = QShortcut(QKeySequence("Escape"), self)
sc_intr.activated.connect(self._do_interrupt)
def _show_camera_frame(self, img_bytes: bytes):
"""Slot — display camera preview overlay (main thread)."""
self._cam_preview.show_frame(img_bytes)
cw = self.centralWidget()
pw = _CameraPreview._W
ph = self._cam_preview.height()
self._cam_preview.setGeometry(
cw.width() - _RIGHT_W - pw - 12,
cw.height() - ph - 28,
pw, ph,
)
# --- Live camera stream in HUD area ------------------------------------
def _on_cam_stream(self, start: bool) -> None:
if start:
self._hud_cam_stack.setCurrentIndex(1)
else:
self._hud_cam_stack.setCurrentIndex(0)
self._cam_live_lbl.clear()
def _on_cam_frame(self, data: bytes) -> None:
px = QPixmap()
px.loadFromData(data)
if not px.isNull():
w, h = self._cam_live_lbl.width(), self._cam_live_lbl.height()
if w > 1 and h > 1:
self._cam_live_lbl.setPixmap(
px.scaled(w, h,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
)
def start_camera_stream(self) -> None:
self._cam_stop.clear()
self._cam_stream_sig.emit(True)
t = threading.Thread(target=self._cam_loop, daemon=True, name="cam-stream")
t.start()
def _cam_loop(self) -> None:
try:
import cv2
# Reuse camera index detected by screen_processor (cached in api_keys.json)
cam_idx = 0
try:
import json as _j
cfg = _j.loads((CONFIG_DIR / "api_keys.json").read_text())
cam_idx = int(cfg.get("camera_index", 0))
except Exception:
pass
try:
backend = cv2.CAP_DSHOW if _OS == "Windows" else cv2.CAP_ANY
except AttributeError:
backend = 0
cap = cv2.VideoCapture(cam_idx, backend)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
return
# warm-up frames
for _ in range(5):
cap.read()
while not self._cam_stop.wait(0.033) and cap.isOpened():
ret, frame = cap.read()
if ret and frame is not None:
_, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 65])
self._cam_frame_sig.emit(buf.tobytes())
cap.release()
except Exception as e:
print(f"[Camera] Stream error: {e}")
finally:
self._cam_stream_sig.emit(False)
def stop_camera_stream(self) -> None:
self._cam_stop.set()
# ------------------------------------------------------------------
# Icon generation — arc-reactor style, rendered with Pillow
# ------------------------------------------------------------------
@staticmethod
def _build_jarvis_icon(out_path: Path) -> bool:
"""
Render a JARVIS arc-reactor icon at 4× resolution and downsample
for crisp results at all sizes. Saves a multi-res .ico to out_path.
Returns True on success.
"""
try:
import math
import PIL.Image
import PIL.ImageDraw
import PIL.ImageFilter
except ImportError:
return False
CYAN = (0, 212, 255)
DIM = (0, 100, 140)
DARK = (0, 6, 10)
GLOW = (0, 160, 200)
WHITE = (220, 240, 255)
def _render(sz: int) -> PIL.Image.Image:
S = sz * 4 # draw at 4× then downscale
img = PIL.Image.new("RGBA", (S, S), (0, 0, 0, 0))
d = PIL.ImageDraw.Draw(img)
cx = cy = S // 2
# ── filled background circle ──────────────────────────────────
R = S // 2 - 2
d.ellipse([cx-R, cy-R, cx+R, cy+R], fill=(*DARK, 255))
# ── outer border ring ─────────────────────────────────────────
lw = max(2, S // 40)
d.ellipse([cx-R, cy-R, cx+R, cy+R],
outline=(*CYAN, 220), width=lw)
# ── mid decorative ring ───────────────────────────────────────
R2 = int(R * 0.72)
d.ellipse([cx-R2, cy-R2, cx+R2, cy+R2],
outline=(*DIM, 180), width=max(1, lw // 2))
# ── 6 radial spokes (hex bolt) ────────────────────────────────
R_inner = int(R * 0.30)
R_outer = int(R * 0.62)
spoke_w = max(1, S // 80)
for i in range(6):
angle = math.radians(i * 60 - 30)
x1 = cx + int(R_inner * math.cos(angle))
y1 = cy + int(R_inner * math.sin(angle))
x2 = cx + int(R_outer * math.cos(angle))
y2 = cy + int(R_outer * math.sin(angle))
d.line([x1, y1, x2, y2], fill=(*GLOW, 200), width=spoke_w)
# ── 6 tick marks on outer ring ────────────────────────────────
for i in range(6):
angle = math.radians(i * 60)
for dr in range(lw * 2):
rx = (R - lw - dr)
d.point(
[cx + int(rx * math.cos(angle)),
cy + int(rx * math.sin(angle))],
fill=(*WHITE, 220),
)
# ── inner glowing ring ────────────────────────────────────────
Ri = int(R * 0.26)
d.ellipse([cx-Ri, cy-Ri, cx+Ri, cy+Ri],
outline=(*CYAN, 255), width=max(2, lw))
# ── bright glow soft blur applied before core ─────────────────
# (draw a slightly larger cyan circle on a separate layer)
glow_layer = PIL.Image.new("RGBA", (S, S), (0, 0, 0, 0))
gd = PIL.ImageDraw.Draw(glow_layer)
Rc = int(R * 0.13)
gd.ellipse([cx-Rc*2, cy-Rc*2, cx+Rc*2, cy+Rc*2],
fill=(*CYAN, 110))
glow_layer = glow_layer.filter(PIL.ImageFilter.GaussianBlur(S // 14))
img = PIL.Image.alpha_composite(img, glow_layer)
d = PIL.ImageDraw.Draw(img)
# ── core dot ──────────────────────────────────────────────────
d.ellipse([cx-Rc, cy-Rc, cx+Rc, cy+Rc], fill=(*WHITE, 255))
# ── downscale to target size ──────────────────────────────────
return img.resize((sz, sz), PIL.Image.LANCZOS)
try:
sizes = [256, 128, 64, 48, 32, 16]
frames = [_render(s) for s in sizes]
frames[0].save(
out_path,
format="ICO",
append_images=frames[1:],
sizes=[(s, s) for s in sizes],
)
return True
except Exception as e:
print(f"[Shortcut] ⚠️ Icon generation failed: {e}")
return False
@staticmethod
def _create_lnk_windows(lnk: str, target: str, args: str,
work_dir: str, icon_loc: str) -> None:
"""
Create a Windows .lnk shortcut WITHOUT launching PowerShell or cmd.
Tries win32com (pywin32) first; falls back to wscript.exe + VBScript.
wscript.exe is a GUI-mode host — it never opens a console window.
"""
# ── Option 1: pywin32 (pure Python COM, zero subprocess) ──────────
try:
from win32com.client import Dispatch # type: ignore
sh = Dispatch("WScript.Shell")
sc = sh.CreateShortCut(lnk)
sc.TargetPath = target
sc.Arguments = f'"{args}"'
sc.WorkingDirectory = work_dir
sc.Description = "J.A.R.V.I.S AI Assistant"
sc.IconLocation = icon_loc
sc.save()
return
except ImportError:
pass
# ── Option 2: wscript.exe + VBScript (always available on Windows,
# GUI-mode executable — never opens a console window) ────────────
vbs = "\n".join([
'Set ws = CreateObject("WScript.Shell")',
f'Set sc = ws.CreateShortcut("{lnk}")',
f'sc.TargetPath = "{target}"',
f'sc.Arguments = Chr(34) & "{args}" & Chr(34)',
f'sc.WorkingDirectory = "{work_dir}"',
'sc.Description = "J.A.R.V.I.S AI Assistant"',
f'sc.IconLocation = "{icon_loc}"',
'sc.Save',
])
import tempfile
fd, tmp = tempfile.mkstemp(suffix=".vbs")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(vbs)
proc = subprocess.Popen(
["wscript.exe", "/nologo", tmp],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
)
proc.wait(timeout=10)
finally:
try:
os.unlink(tmp)
except Exception:
pass
def _create_desktop_shortcut(self):
"""
Create a desktop shortcut on Windows / macOS / Linux.
Never opens a terminal, console, or PowerShell window on any platform.
"""
import stat as _stat
script = Path(__file__).resolve().parent / "main.py"
python = Path(sys.executable)
desktop = Path.home() / "Desktop"
# Arc-reactor icon (.ico — also exported as .png for Linux/macOS)
ico_path = Path(__file__).resolve().parent / "config" / "jarvis.ico"
if not ico_path.exists():
self._build_jarvis_icon(ico_path)
try:
_os = platform.system()
# ── Windows ───────────────────────────────────────────────────────
if _os == "Windows":
pythonw = python.parent / "pythonw.exe"
target = str(pythonw if pythonw.exists() else python)
lnk = str(desktop / "J.A.R.V.I.S.lnk")
icon_loc = str(ico_path) if ico_path.exists() else f"{target},0"
self._create_lnk_windows(lnk, target, str(script),
str(script.parent), icon_loc)
# ── macOS — proper .app bundle (no Terminal window) ───────────────
elif _os == "Darwin":
app = desktop / "J.A.R.V.I.S.app"
mac_dir = app / "Contents" / "MacOS"
res_dir = app / "Contents" / "Resources"
mac_dir.mkdir(parents=True, exist_ok=True)
res_dir.mkdir(exist_ok=True)
# Launcher executable (bash — runs as background process,
# macOS does NOT open Terminal for executables inside .app bundles)
launcher = mac_dir / "JARVIS"
launcher.write_text(
"#!/usr/bin/env bash\n"
f'cd "{script.parent}"\n'
f'exec "{python}" "{script}"\n'
)
launcher.chmod(launcher.stat().st_mode
| _stat.S_IEXEC | _stat.S_IXGRP | _stat.S_IXOTH)
# Minimal Info.plist (required for .app recognition)
(app / "Contents" / "Info.plist").write_text(
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
'<plist version="1.0"><dict>\n'
' <key>CFBundleExecutable</key><string>JARVIS</string>\n'
' <key>CFBundleIdentifier</key>'
'<string>com.jarvis.assistant</string>\n'
' <key>CFBundleName</key><string>J.A.R.V.I.S</string>\n'
' <key>CFBundlePackageType</key><string>APPL</string>\n'
' <key>CFBundleVersion</key><string>1.0</string>\n'
'</dict></plist>\n'
)
# Optional: copy icon as .icns (skip silently if Pillow is missing)
try:
import PIL.Image
icns = res_dir / "AppIcon.icns"
PIL.Image.open(ico_path).save(icns, format="ICNS")
# Inject icon reference into plist
plist = app / "Contents" / "Info.plist"
txt = plist.read_text()
plist.write_text(
txt.replace(
'</dict></plist>',
' <key>CFBundleIconFile</key>'
'<string>AppIcon</string>\n</dict></plist>\n',
)
)
except Exception:
pass # icon is optional
# ── Linux — .desktop file (Terminal=false, no console) ────────────
else:
# Export .ico → .png for better desktop integration
png_path = ico_path.with_suffix(".png")
if not png_path.exists() and ico_path.exists():
try:
import PIL.Image
PIL.Image.open(ico_path).resize(
(256, 256), PIL.Image.LANCZOS
).save(png_path, format="PNG")
except Exception:
png_path = ico_path # fallback to .ico
icon_line = f"Icon={png_path}\n" if png_path.exists() else ""
desk = desktop / "J.A.R.V.I.S.desktop"
desk.write_text(
"[Desktop Entry]\n"
"Name=J.A.R.V.I.S\n"
f"Exec={python} {script}\n"
f"Path={script.parent}\n"
"Type=Application\n"
"Terminal=false\n"
"Categories=Utility;\n"
+ icon_line
)
desk.chmod(desk.stat().st_mode | 0o755)
self._log.append_log("SYS: Desktop shortcut created.")
except Exception as e:
self._log.append_log(f"ERR: Shortcut failed — {e}")
def _toggle_fullscreen(self):
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
def resizeEvent(self, event):
super().resizeEvent(event)
cw = self.centralWidget()
if self._overlay and self._overlay.isVisible():
ow, oh = 460, 390
self._overlay.setGeometry(
(cw.width() - ow) // 2,
(cw.height() - oh) // 2,
ow, oh,
)
if self._remote_overlay and self._remote_overlay.isVisible():
ow, oh = RemoteKeyOverlay._OW, RemoteKeyOverlay._OH
self._remote_overlay.setGeometry(
(cw.width() - ow) // 2,
(cw.height() - oh) // 2,
ow, oh,
)
# Camera preview — bottom-right corner of the center/HUD area
pw = _CameraPreview._W
ph = self._cam_preview.height() or _CameraPreview._H
self._cam_preview.setGeometry(
cw.width() - _RIGHT_W - pw - 12,
cw.height() - ph - 28,
pw, ph,
)
def _update_metrics(self):
snap = _metrics.snapshot()
# CPU
cpu = snap["cpu"]
self._bar_cpu.set_value(cpu, f"{cpu:.0f}%")
# MEM
mem = snap["mem"]
self._bar_mem.set_value(mem, f"{mem:.0f}%")
# NET
net = snap["net"]
if net < 1.0:
net_str = f"{net*1024:.0f}KB/s"
else:
net_str = f"{net:.1f}MB/s"
net_pct = min(100, net * 10) # 10 MB/s = %100
self._bar_net.set_value(net_pct, net_str)
# GPU
gpu = snap["gpu"]
if gpu >= 0:
self._bar_gpu.set_value(gpu, f"{gpu:.0f}%")
else:
self._bar_gpu.set_value(0, "N/A")
# TMP
tmp = snap["tmp"]
if tmp >= 0:
tmp_pct = min(100, (tmp / 100) * 100)
self._bar_tmp.set_value(tmp_pct, f"{tmp:.0f}°C")
else:
self._bar_tmp.set_value(0, "N/A")
try:
boot_t = psutil.boot_time()
elapsed = time.time() - boot_t
h = int(elapsed // 3600)
m = int((elapsed % 3600) // 60)
self._uptime_lbl.setText(f"UP {h:02d}:{m:02d}")
except Exception:
self._uptime_lbl.setText("UP --:--")
try:
proc_count = len(psutil.pids())
self._proc_lbl.setText(f"PROC {proc_count}")
except Exception:
self._proc_lbl.setText("PROC --")
def _build_header(self) -> QWidget:
w = QWidget()
w.setFixedHeight(54)
w.setStyleSheet(f"background: {C.DARK}; border-bottom: 1px solid {C.BORDER_B};")
lay = QHBoxLayout(w)
lay.setContentsMargins(16, 0, 16, 0)
def _badge(txt, color=C.TEXT_MED):
l = QLabel(txt)
l.setFont(QFont("Courier New", 8))
l.setStyleSheet(f"color: {color}; background: transparent;")
return l
lay.addWidget(_badge("MARK XLVIII", C.PRI_DIM))
lay.addStretch()
mid = QVBoxLayout(); mid.setSpacing(1)
title = QLabel("J.A.R.V.I.S")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setFont(QFont("Courier New", 17, QFont.Weight.Bold))
title.setStyleSheet(f"color: {C.PRI}; background: transparent;")
mid.addWidget(title)
sub = QLabel("Just A Rather Very Intelligent System")
sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
sub.setFont(QFont("Courier New", 7))
sub.setStyleSheet(f"color: {C.PRI_DIM}; background: transparent;")
mid.addWidget(sub)
lay.addLayout(mid)
lay.addStretch()
right_col = QVBoxLayout(); right_col.setSpacing(2)
self._clock_lbl = QLabel("00:00:00")
self._clock_lbl.setFont(QFont("Courier New", 14, QFont.Weight.Bold))
self._clock_lbl.setStyleSheet(f"color: {C.PRI}; background: transparent;")
self._clock_lbl.setAlignment(Qt.AlignmentFlag.AlignRight)
right_col.addWidget(self._clock_lbl)
self._date_lbl = QLabel("")
self._date_lbl.setFont(QFont("Courier New", 7))
self._date_lbl.setStyleSheet(f"color: {C.TEXT_DIM}; background: transparent;")
self._date_lbl.setAlignment(Qt.AlignmentFlag.AlignRight)
right_col.addWidget(self._date_lbl)
lay.addLayout(right_col)
return w
def _tick_clock(self):
self._clock_lbl.setText(time.strftime("%H:%M:%S"))
self._date_lbl.setText(time.strftime("%a %d %b %Y"))
def _build_left_panel(self) -> QWidget:
w = QWidget()
w.setFixedWidth(_LEFT_W)
w.setStyleSheet(f"background: {C.DARK}; border-right: 1px solid {C.BORDER};")
lay = QVBoxLayout(w)
lay.setContentsMargins(8, 10, 8, 10)
lay.setSpacing(6)
hdr = QLabel("◈ SYS MONITOR")
hdr.setFont(QFont("Courier New", 7, QFont.Weight.Bold))
hdr.setStyleSheet(f"color: {C.PRI}; background: transparent; "
f"border-bottom: 1px solid {C.BORDER}; padding-bottom: 4px;")
lay.addWidget(hdr)
lay.addSpacing(2)
self._bar_cpu = MetricBar("CPU", C.PRI)
self._bar_mem = MetricBar("MEM", C.ACC2)
self._bar_net = MetricBar("NET", C.GREEN)
self._bar_gpu = MetricBar("GPU", C.ACC)
self._bar_tmp = MetricBar("TMP", "#ff6688")
for bar in [self._bar_cpu, self._bar_mem, self._bar_net,
self._bar_gpu, self._bar_tmp]:
lay.addWidget(bar)
lay.addSpacing(4)
info_panel = QWidget()
info_panel.setStyleSheet(
f"background: {C.PANEL2}; border: 1px solid {C.BORDER}; border-radius: 4px;"
)
ip_lay = QVBoxLayout(info_panel)
ip_lay.setContentsMargins(6, 5, 6, 5)
ip_lay.setSpacing(3)
self._uptime_lbl = QLabel("UP --:--")
self._uptime_lbl.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
self._uptime_lbl.setStyleSheet(f"color: {C.GREEN}; background: transparent; border: none;")
ip_lay.addWidget(self._uptime_lbl)
self._proc_lbl = QLabel("PROC --")
self._proc_lbl.setFont(QFont("Courier New", 8))
self._proc_lbl.setStyleSheet(f"color: {C.TEXT_MED}; background: transparent; border: none;")
ip_lay.addWidget(self._proc_lbl)
os_name = {"Windows": "WIN", "Darwin": "macOS", "Linux": "LINUX"}.get(_OS, _OS.upper())
os_lbl = QLabel(f"OS {os_name}")
os_lbl.setFont(QFont("Courier New", 8))
os_lbl.setStyleSheet(f"color: {C.ACC2}; background: transparent; border: none;")
ip_lay.addWidget(os_lbl)
lay.addWidget(info_panel)
lay.addSpacing(4)
lay.addStretch()
for txt, col in [
("AI CORE\nACTIVE", C.GREEN),
("SEC\nCLEARED", C.PRI),
("PROTOCOL\nXLVIII", C.TEXT_DIM),
]:
lbl = QLabel(txt)
lbl.setFont(QFont("Courier New", 7, QFont.Weight.Bold))
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
lbl.setStyleSheet(
f"color: {col}; background: {C.PANEL2};"
f"border: 1px solid {C.BORDER_A}; border-radius: 3px; padding: 4px;"
)
lay.addWidget(lbl)
return w
def _build_right_panel(self) -> QWidget:
w = QWidget()
w.setFixedWidth(_RIGHT_W)
w.setStyleSheet(f"background: {C.DARK}; border-left: 1px solid {C.BORDER};")
lay = QVBoxLayout(w)
lay.setContentsMargins(8, 8, 8, 8)
lay.setSpacing(6)
def _sec(txt):
l = QLabel(f"▸ {txt}")
l.setFont(QFont("Courier New", 7, QFont.Weight.Bold))
l.setStyleSheet(f"color: {C.TEXT_MED}; background: transparent;")
return l
lay.addWidget(_sec("ACTIVITY LOG"))
self._log = LogWidget()
lay.addWidget(self._log, stretch=1)
sep = QFrame(); sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet(f"color: {C.BORDER}; margin: 2px 0;")
lay.addWidget(sep)
lay.addWidget(_sec("FILE UPLOAD"))
self._drop_zone = FileDropZone()
self._drop_zone.file_selected.connect(self._on_file_selected)
lay.addWidget(self._drop_zone)
self._file_hint = QLabel("No file loaded — drop or click above to upload")
self._file_hint.setFont(QFont("Courier New", 7))
self._file_hint.setStyleSheet(f"color: {C.TEXT_MED}; background: transparent;")
self._file_hint.setWordWrap(True)
lay.addWidget(self._file_hint)
sep2 = QFrame(); sep2.setFrameShape(QFrame.Shape.HLine)
sep2.setStyleSheet(f"color: {C.BORDER}; margin: 2px 0;")
lay.addWidget(sep2)
lay.addWidget(_sec("COMMAND INPUT"))
lay.addLayout(self._build_input_row())
self._interrupt_btn = QPushButton("✋ INTERRUPT [ESC]")
self._interrupt_btn.setFixedHeight(34)
self._interrupt_btn.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
self._interrupt_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self._interrupt_btn.setStyleSheet(f"""
QPushButton {{
background: #140008; color: {C.MUTED_C};
border: 1px solid {C.MUTED_C}; border-radius: 3px;
}}
QPushButton:hover {{
background: #200010; border: 1px solid #ff6688;
}}
QPushButton:pressed {{
background: #300018;
}}
""")
self._interrupt_btn.clicked.connect(self._do_interrupt)
lay.addWidget(self._interrupt_btn)
self._mute_btn = QPushButton("🎙 MICROPHONE ACTIVE")
self._mute_btn.setFixedHeight(30)
self._mute_btn.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
self._mute_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self._mute_btn.clicked.connect(self._toggle_mute)
self._style_mute_btn()
lay.addWidget(self._mute_btn)
remote_btn = QPushButton("◉ REMOTE CONTROL")
remote_btn.setFixedHeight(30)
remote_btn.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
remote_btn.setCursor(Qt.CursorShape.PointingHandCursor)
remote_btn.setStyleSheet(f"""
QPushButton {{
background: #00091a; color: {C.PRI};
border: 1px solid {C.PRI_DIM}; border-radius: 3px;
}}
QPushButton:hover {{
background: {C.PRI_GHO}; border: 1px solid {C.PRI};
}}
""")
remote_btn.clicked.connect(self._open_remote)
lay.addWidget(remote_btn)
fs_btn = QPushButton("⛶ FULLSCREEN [F11]")
fs_btn.setFixedHeight(26)
fs_btn.setFont(QFont("Courier New", 7))
fs_btn.setCursor(Qt.CursorShape.PointingHandCursor)
fs_btn.setStyleSheet(f"""
QPushButton {{
background: transparent; color: {C.TEXT_MED};
border: 1px solid {C.BORDER}; border-radius: 3px;
}}
QPushButton:hover {{
color: {C.PRI}; border: 1px solid {C.BORDER_B};
}}
""")
fs_btn.clicked.connect(self._toggle_fullscreen)
lay.addWidget(fs_btn)
sc_btn = QPushButton("⊞ CREATE DESKTOP SHORTCUT")
sc_btn.setFixedHeight(26)
sc_btn.setFont(QFont("Courier New", 7))
sc_btn.setCursor(Qt.CursorShape.PointingHandCursor)
sc_btn.setStyleSheet(f"""
QPushButton {{
background: transparent; color: {C.TEXT_DIM};
border: 1px solid {C.BORDER}; border-radius: 3px;
}}
QPushButton:hover {{
color: {C.ACC2}; border: 1px solid {C.BORDER_B};
}}
""")
sc_btn.clicked.connect(self._create_desktop_shortcut)
lay.addWidget(sc_btn)
return w
def _build_input_row(self) -> QHBoxLayout:
row = QHBoxLayout(); row.setSpacing(5)
self._input = QLineEdit()
self._input.setPlaceholderText("Type a command or question…")
self._input.setFont(QFont("Courier New", 9))
self._input.setFixedHeight(30)
self._input.setStyleSheet(f"""
QLineEdit {{
background: #000d14; color: {C.WHITE};
border: 1px solid {C.BORDER}; border-radius: 3px; padding: 3px 7px;
}}
QLineEdit:focus {{ border: 1px solid {C.PRI}; }}
""")
self._input.returnPressed.connect(self._send)
row.addWidget(self._input)
send = QPushButton("▸")
send.setFixedSize(30, 30)
send.setFont(QFont("Courier New", 11, QFont.Weight.Bold))
send.setCursor(Qt.CursorShape.PointingHandCursor)
send.setStyleSheet(f"""
QPushButton {{
background: {C.PANEL}; color: {C.PRI};
border: 1px solid {C.PRI_DIM}; border-radius: 3px;
}}
QPushButton:hover {{ background: {C.PRI_GHO}; border: 1px solid {C.PRI}; }}
""")
send.clicked.connect(self._send)
row.addWidget(send)
return row
def _build_content_panel(self) -> QWidget:
"""
Collapsible panel below the HUD — shows search results, news, briefings.
Hidden by default; appears when show_content() is called.
"""
w = QWidget()
w.setObjectName("ContentPanel")
w.setStyleSheet(f"""
QWidget#ContentPanel {{
background: {C.PANEL};
border-top: 1px solid {C.BORDER_B};
}}
""")
w.hide()
lay = QVBoxLayout(w)
lay.setContentsMargins(12, 7, 12, 8)
lay.setSpacing(5)
# ── header row ───────────────────────────────────────────────────────
hdr = QHBoxLayout(); hdr.setSpacing(6)
dot = QLabel("◈")
dot.setFont(QFont("Courier New", 9, QFont.Weight.Bold))
dot.setStyleSheet(f"color: {C.PRI}; background: transparent;")
hdr.addWidget(dot)
self._content_title_lbl = QLabel("BRIEFING")
self._content_title_lbl.setFont(QFont("Courier New", 8, QFont.Weight.Bold))
self._content_title_lbl.setStyleSheet(
f"color: {C.PRI}; background: transparent; letter-spacing: 1px;"
)
hdr.addWidget(self._content_title_lbl)
hdr.addStretch()
self._content_ts_lbl = QLabel("")
self._content_ts_lbl.setFont(QFont("Courier New", 7))
self._content_ts_lbl.setStyleSheet(f"color: {C.TEXT_DIM}; background: transparent;")
hdr.addWidget(self._content_ts_lbl)
dismiss = QPushButton("DISMISS ✕")
dismiss.setFont(QFont("Courier New", 7))
dismiss.setFixedHeight(18)
dismiss.setCursor(Qt.CursorShape.PointingHandCursor)
dismiss.setStyleSheet(f"""
QPushButton {{
background: transparent; color: {C.TEXT_DIM};
border: 1px solid {C.BORDER}; border-radius: 2px; padding: 0 5px;
}}
QPushButton:hover {{ color: {C.TEXT}; border-color: {C.BORDER_B}; }}
""")
dismiss.clicked.connect(w.hide)
hdr.addWidget(dismiss)
lay.addLayout(hdr)
# ── separator ─────────────────────────────────────────────────────────
sep = QFrame(); sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet(f"color: {C.BORDER};"); lay.addWidget(sep)
# ── text display ──────────────────────────────────────────────────────
self._content_display = QTextEdit()
self._content_display.setReadOnly(True)
self._content_display.setFont(QFont("Courier New", 8))
self._content_display.setMinimumHeight(60)
self._content_display.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
self._content_display.setStyleSheet(f"""
QTextEdit {{
background: {C.DARK};
color: {C.TEXT};
border: 1px solid {C.BORDER};
border-radius: 3px;
padding: 6px 8px;
selection-background-color: {C.PRI_GHO};
}}
QScrollBar:vertical {{
background: {C.BG}; width: 6px; border: none;
}}
QScrollBar::handle:vertical {{
background: {C.BORDER_B}; border-radius: 3px; min-height: 16px;
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0; border: none;
}}
""")
lay.addWidget(self._content_display)
return w
def _show_content(self, title: str, text: str):
"""Slot — runs on Qt main thread. Updates and shows the content panel."""
import time as _time
self._content_title_lbl.setText(title.upper()[:48])
self._content_ts_lbl.setText(_time.strftime("%H:%M:%S"))
self._content_display.setPlainText(text)
self._content_display.moveCursor(
self._content_display.textCursor().MoveOperation.Start
)
first_show = not self._content_panel.isVisible()
self._content_panel.show()
if first_show:
total = self._center_split.height()
self._center_split.setSizes([max(total - 220, 120), 220])
def _build_footer(self) -> QWidget:
w = QWidget()
w.setFixedHeight(22)
w.setStyleSheet(f"background: {C.DARK}; border-top: 1px solid {C.BORDER};")
lay = QHBoxLayout(w); lay.setContentsMargins(14, 0, 14, 0)
def _fl(txt, color=C.TEXT_MED):
l = QLabel(txt); l.setFont(QFont("Courier New", 7))
l.setStyleSheet(f"color: {color}; background: transparent;")
return l
lay.addWidget(_fl("[F4] Mute · [F11] Fullscreen"))
lay.addStretch()
lay.addWidget(_fl("FatihMakes Industries · MARK XLVIII · CLASSIFIED"))
lay.addStretch()
lay.addWidget(_fl("© STARK INDUSTRIES", C.PRI_DIM))
return w
def _on_file_selected(self, path: str):
self._current_file = path
p = Path(path)
cat = _file_category(p)
icon, _ = _FILE_ICONS.get(cat, _FILE_ICONS["unknown"])
size = _fmt_size(p.stat().st_size)
self._file_hint.setText(f"{icon} {p.name} · {size} · Tell JARVIS what to do with it")
self._log.append_log(f"FILE: {p.name} ({size}) loaded")
if self.on_text_command:
msg = (
f"[FILE_UPLOADED] path={path} | name={p.name} | "
f"type={p.suffix.lstrip('.')} | size={size} | "
f"Briefly tell the user you can see the file '{p.name}' "
f"({size}) has been uploaded and ask what they'd like to do with it."
)
threading.Thread(target=self.on_text_command, args=(msg,), daemon=True).start()
def notify_phone_connected(self) -> None:
if self._remote_overlay and self._remote_overlay.isVisible():
self._remote_overlay.mark_connected()
def _open_remote(self):
if not self.on_remote_clicked:
self._log.append_log("SYS: Dashboard not running — remote unavailable.")
return
result = self.on_remote_clicked()
if not result:
self._log.append_log("SYS: Could not generate remote key.")
return
url = result[0]
key = result[1]
auto = result[2] if len(result) >= 3 else ""
manual = result[3] if len(result) >= 4 else url
if self._remote_overlay:
self._remote_overlay._do_close()
cw = self.centralWidget()
ow, oh = RemoteKeyOverlay._OW, RemoteKeyOverlay._OH
ov = RemoteKeyOverlay(url, key, auto_login_url=auto, manual_url=manual,
expiry_secs=600, parent=cw)
ov.set_new_key_callback(self.on_remote_clicked)
ov.setGeometry(
(cw.width() - ow) // 2,
(cw.height() - oh) // 2,
ow, oh,
)
ov.closed.connect(lambda: setattr(self, '_remote_overlay', None))
ov.show()
self._remote_overlay = ov
self._log.append_log(f"SYS: Remote key generated — manual: {manual or url}")
def _do_interrupt(self):
if self.on_interrupt:
self.on_interrupt()
def _toggle_mute(self):
self._muted = not self._muted
self.hud.muted = self._muted
self._style_mute_btn()
if self._muted:
self._apply_state("MUTED")
self._log.append_log("SYS: Microphone muted.")
else:
self._apply_state("LISTENING")
self._log.append_log("SYS: Microphone active.")
def _style_mute_btn(self):
if self._muted:
self._mute_btn.setText("🔇 MICROPHONE MUTED")
self._mute_btn.setStyleSheet(f"""
QPushButton {{
background: #140006; color: {C.MUTED_C};
border: 1px solid {C.MUTED_C}; border-radius: 3px;
}}
""")
else:
self._mute_btn.setText("🎙 MICROPHONE ACTIVE")
self._mute_btn.setStyleSheet(f"""
QPushButton {{
background: #00140a; color: {C.GREEN};
border: 1px solid {C.GREEN}; border-radius: 3px;
}}
QPushButton:hover {{ background: #001f10; }}
""")
def _send(self):
txt = self._input.text().strip()
if not txt: return
self._input.clear()
self._log.append_log(f"You: {txt}")
if self.on_text_command:
threading.Thread(target=self.on_text_command, args=(txt,), daemon=True).start()
def _apply_state(self, state: str):
self.hud.state = state
self.hud.speaking = (state == "SPEAKING")
def _check_config(self) -> bool:
if not API_FILE.exists(): return False
try:
d = json.loads(API_FILE.read_text(encoding="utf-8"))
return bool(d.get("gemini_api_key")) and bool(d.get("os_system"))
except Exception:
return False
def _show_setup(self):
ov = SetupOverlay(self.centralWidget())
cw = self.centralWidget()
ow, oh = 460, 390
ov.setGeometry(
(cw.width() - ow) // 2,
(cw.height() - oh) // 2,
ow, oh,
)
ov.done.connect(self._on_setup_done)
ov.show()
self._overlay = ov
def _on_setup_done(self, key: str, os_name: str):
os.makedirs(CONFIG_DIR, exist_ok=True)
API_FILE.write_text(
json.dumps({"gemini_api_key": key, "os_system": os_name}, indent=4),
encoding="utf-8",
)
self._ready = True
if self._overlay:
self._overlay.hide()
self._overlay = None
self._apply_state("LISTENING")
self._log.append_log(f"SYS: Initialised. OS={os_name.upper()}. JARVIS online.")
class _RootShim:
def __init__(self, app: QApplication):
self._app = app
def mainloop(self):
self._app.exec()
def protocol(self, *_):
pass
class JarvisUI:
def __init__(self, face_path: str, size=None):
self._app = QApplication.instance() or QApplication(sys.argv)
self._app.setStyle("Fusion")
self._win = MainWindow(face_path)
self._win.show()
self.root = _RootShim(self._app)
@property
def muted(self) -> bool:
return self._win._muted
@muted.setter
def muted(self, v: bool):
if v != self._win._muted:
self._win._toggle_mute()
@property
def current_file(self) -> str | None:
return self._win._drop_zone.current_file()
@property
def on_text_command(self):
return self._win.on_text_command
@on_text_command.setter
def on_text_command(self, cb):
self._win.on_text_command = cb
@property
def on_remote_clicked(self):
return self._win.on_remote_clicked
@on_remote_clicked.setter
def on_remote_clicked(self, cb):
self._win.on_remote_clicked = cb
@property
def on_interrupt(self):
return self._win.on_interrupt
@on_interrupt.setter
def on_interrupt(self, cb):
self._win.on_interrupt = cb
def notify_phone_connected(self) -> None:
self._win.notify_phone_connected()
def set_state(self, state: str):
self._win._state_sig.emit(state)
def write_log(self, text: str):
self._win._log_sig.emit(text)
def wait_for_api_key(self):
while not self._win._ready:
time.sleep(0.1)
def show_content(self, title: str, text: str):
"""Thread-safe: display content in the panel below the HUD."""
self._win._content_sig.emit(title[:48], text[:4000])
def prompt_reconfig(self):
"""Thread-safe: show the API key setup overlay (e.g. after an auth error)."""
self._win._ready = False
self._win._reconfig_sig.emit()
def show_camera_frame(self, img_bytes: bytes):
"""Thread-safe: show a webcam frame in the small overlay (screen captures)."""
self._win._camera_sig.emit(img_bytes)
def start_camera_stream(self) -> None:
"""Thread-safe: start live camera feed in the full HUD area."""
self._win.start_camera_stream()
def stop_camera_stream(self) -> None:
"""Thread-safe: stop the live camera feed."""
self._win.stop_camera_stream()
def start_speaking(self):
self.set_state("SPEAKING")
def stop_speaking(self):
if not self.muted:
self.set_state("LISTENING")