diff --git a/JiboTools/JiboTools/.gitignore b/JiboTools/JiboTools/.gitignore new file mode 100644 index 0000000..ca8b61e --- /dev/null +++ b/JiboTools/JiboTools/.gitignore @@ -0,0 +1,84 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash +**/.qmlls.ini + +# qtcreator generated files +*.pro.user* +*.qbs.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Directories with generated files +.moc/ +.obj/ +.pch/ +.rcc/ +.uic/ +/build*/ +/.qtcreator/ diff --git a/JiboTools/JiboTools/form.ui b/JiboTools/JiboTools/form.ui new file mode 100644 index 0000000..f6b9cb5 --- /dev/null +++ b/JiboTools/JiboTools/form.ui @@ -0,0 +1,643 @@ + + + MainWindow + + + + 0 + 0 + 782 + 649 + + + + Jibo Tools + + + QTabWidget::TabShape::Rounded + + + + + 12 + + + 14 + + + 14 + + + 14 + + + 14 + + + + + 0 + + + true + + + false + + + false + + + + Jibo + + + + 14 + + + + + + 420 + 0 + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 18 + + + 18 + + + 18 + + + 18 + + + + + + 12 + true + + + + Config + + + + + + + true + + + + + 0 + 0 + 380 + 478 + + + + + 14 + + + + + Preview + + + + + + Override + + + + + + + + + + true + + + + + + + Connected + + + + + + + + + + + + + + + + + Home Assistant + + + + + + Enabled + + + + + + + + + + + + + + Server IP + + + + + + + Home Assistant host + + + + + + + + + + AI Provider + + + + + + Enabled + + + + + + + + + + + + + + Provider + + + + + + + + + + API endpoint + + + + + + + http://... + + + + + + + API key + + + + + + + QLineEdit::EchoMode::Password + + + + + + + Tokens used + + + + + + + -1 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 12 + + + 18 + + + 18 + + + 18 + + + 18 + + + + + + + Connect + + + false + + + + + + + + + + + + + e.g 192.168.1.54 + + + + + + + + + + + + 12 + true + false + + + + Connect Your Jibo + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 260 + 260 + + + + + 260 + 260 + + + + + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Minimum + + + + 20 + 40 + + + + + + + + false + + + Robot Settings + + + false + + + + + + + false + + + true + + + Reboot + + + Reboot + + + + + + + + + + + Update + + + + 12 + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 12 + + + 18 + + + 18 + + + 18 + + + 18 + + + + + Installer and updater remain available via CLI. Use the buttons below to launch their GUIs. + + + true + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + 12 + + + + + Install + + + + + + + Check for updates + + + + + + + + + + + + + Skills + + + + + + Coming soon. + + + + + + + + SSH + + + + + + Coming soon. + + + + + + + + FTP + + + + + + Coming soon. + + + + + + + + Status + + + + 10 + + + + + 10 + + + + + + 10 + 10 + + + + + 10 + 10 + + + + + + + + + + + No Jibo IP configured + + + true + + + + + + + + + + Robot OS + + + + + + Coming soon. + + + + + + + + + + + + false + + + + + + diff --git a/JiboTools/JiboTools/gui/Assets/Jibo/Archive.zip b/JiboTools/JiboTools/gui/Assets/Jibo/Archive.zip new file mode 100644 index 0000000..ef52089 Binary files /dev/null and b/JiboTools/JiboTools/gui/Assets/Jibo/Archive.zip differ diff --git a/JiboTools/JiboTools/gui/Assets/Jibo/JiboFaceForward.png b/JiboTools/JiboTools/gui/Assets/Jibo/JiboFaceForward.png new file mode 100644 index 0000000..a97a45b Binary files /dev/null and b/JiboTools/JiboTools/gui/Assets/Jibo/JiboFaceForward.png differ diff --git a/JiboTools/JiboTools/gui/Assets/Jibo/NoJiboConnected.png b/JiboTools/JiboTools/gui/Assets/Jibo/NoJiboConnected.png new file mode 100644 index 0000000..2af7541 Binary files /dev/null and b/JiboTools/JiboTools/gui/Assets/Jibo/NoJiboConnected.png differ diff --git a/gui/__init__.py b/JiboTools/JiboTools/gui/__init__.py similarity index 100% rename from gui/__init__.py rename to JiboTools/JiboTools/gui/__init__.py diff --git a/JiboTools/JiboTools/gui/assets/jibo.kra b/JiboTools/JiboTools/gui/assets/jibo.kra new file mode 100644 index 0000000..2130a7f Binary files /dev/null and b/JiboTools/JiboTools/gui/assets/jibo.kra differ diff --git a/gui/assets/jibo.svg b/JiboTools/JiboTools/gui/assets/jibo.svg similarity index 100% rename from gui/assets/jibo.svg rename to JiboTools/JiboTools/gui/assets/jibo.svg diff --git a/JiboTools/JiboTools/gui/installer_gui.py b/JiboTools/JiboTools/gui/installer_gui.py new file mode 100644 index 0000000..e3d3112 --- /dev/null +++ b/JiboTools/JiboTools/gui/installer_gui.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import sys +from PySide6.QtWidgets import QApplication + +from .tool_runner_window import ToolRunnerWindow + + +def main() -> int: + app = QApplication(sys.argv) + win = ToolRunnerWindow(title="Installer", script="jibo_automod.py") + win.show() + return int(app.exec()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/JiboTools/JiboTools/gui/main_panel.py b/JiboTools/JiboTools/gui/main_panel.py new file mode 100644 index 0000000..49427a1 --- /dev/null +++ b/JiboTools/JiboTools/gui/main_panel.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path +from typing import Optional + +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QTabWidget, +) + +from .process_runner import resolve_python, resolve_python_invocation +from .ui_loader import load_ui, require_child + + +def _set_dot(label: QLabel, color: str) -> None: + label.setStyleSheet( + "QLabel {" + f"background-color: {color};" + "border-radius: 5px;" + "}" + ) + + +class MainWindowController: + def __init__(self) -> None: + project_root = Path(__file__).resolve().parents[1] + ui_path = project_root / "form.ui" + self.window = load_ui(ui_path) + if not hasattr(self.window, "setWindowTitle"): + raise RuntimeError("form.ui must have a QMainWindow root") + + self._ssh_client: Optional[object] = None + self._identity: Optional[dict] = None + self._connecting = False + + # Tabs + connection pill + self.tab_widget = require_child(self.window, "tabWidget", QTabWidget) + self.connection_pill, self.conn_dot, self.conn_text = self._create_connection_pill() + self.tab_widget.setCornerWidget(self.connection_pill, Qt.TopRightCorner) + + # Jibo/config + self.jibo_ip = require_child(self.window, "JiboIpField", QLineEdit) + self.connect_button = require_child(self.window, "TryToConnect", QPushButton) + self.jibo_title = require_child(self.window, "jiboTitle", QLabel) + self.override_check = require_child(self.window, "overrideCheck", QCheckBox) + self.preview_connected_check = require_child(self.window, "previewConnectedCheck", QCheckBox) + + self.ha_enable = require_child(self.window, "haEnableCheck", QCheckBox) + self.ha_server_ip = require_child(self.window, "haServerIpField", QLineEdit) + + self.ai_enable = require_child(self.window, "aiEnableCheck", QCheckBox) + self.ai_provider = require_child(self.window, "aiProviderCombo", QComboBox) + self.ai_endpoint = require_child(self.window, "aiEndpointField", QLineEdit) + self.ai_key = require_child(self.window, "aiKeyField", QLineEdit) + self.tokens_used = require_child(self.window, "tokensUsedLabel", QLabel) + + # Jibo card controls + self.robot_settings_button = require_child(self.window, "RobotSettings", QPushButton) + self.robot_action_combo = require_child(self.window, "comboBox", QComboBox) + self.jibo_image = require_child(self.window, "jiboImage", QLabel) + + # Update page + self.install_button = require_child(self.window, "installButton", QPushButton) + self.check_updates_button = require_child(self.window, "checkUpdatesButton", QPushButton) + + # Status page + self.status_dot = require_child(self.window, "statusDot", QLabel) + self.status_text = require_child(self.window, "statusText", QLabel) + + self._configure_ui() + self._wire_signals() + self._sync_enabled() + self._sync_all() + + @property + def host(self) -> str: + return self.jibo_ip.text().strip() + + @property + def session_connected(self) -> bool: + return self._ssh_client is not None + + def effective_connected(self) -> bool: + """Effective connection state for visuals. + + The Preview override is kept for UI testing, but the real connect/ + disconnect state comes from an active SSH session. + """ + + if self.override_check.isChecked(): + return self.preview_connected_check.isChecked() + return self.session_connected + + def _configure_ui(self) -> None: + # Simple styling, roughly matching the previous QML look. + self.connection_pill.setStyleSheet( + "QFrame#connectionPill {" + "background-color: #f6f6f6;" + "border: 1px solid #e4e4e4;" + "border-radius: 14px;" + "}" + ) + + # Provider choices + self.ai_provider.clear() + self.ai_provider.addItems(["Self-hosted", "OpenAI", "Other"]) + + # Robot controls start disabled until connected. + self.robot_settings_button.setEnabled(False) + self.robot_action_combo.setEnabled(False) + + # Defaults + self.tokens_used.setText("-1") + self.connect_button.setText("Connect") + self.jibo_title.setText("Connect Your Jibo") + + def _wire_signals(self) -> None: + self.connect_button.clicked.connect(self._toggle_connection) + + self.override_check.toggled.connect(self._sync_enabled) + self.preview_connected_check.toggled.connect(self._sync_all) + self.override_check.toggled.connect(self._sync_all) + + self.ha_enable.toggled.connect(self._sync_enabled) + self.ai_enable.toggled.connect(self._sync_enabled) + + self.install_button.clicked.connect(self._launch_installer) + self.check_updates_button.clicked.connect(self._launch_updater) + + def _sync_enabled(self) -> None: + self.preview_connected_check.setEnabled(self.override_check.isChecked()) + self.ha_server_ip.setEnabled(self.ha_enable.isChecked()) + + ai_enabled = self.ai_enable.isChecked() + self.ai_provider.setEnabled(ai_enabled) + self.ai_endpoint.setEnabled(ai_enabled) + self.ai_key.setEnabled(ai_enabled) + + # Connection button enabled unless a connect attempt is in progress. + self.connect_button.setEnabled(not self._connecting) + + def _sync_all(self) -> None: + host = self.host + connected = self.session_connected + visual_connected = self.effective_connected() + + if connected: + title = (self._identity or {}).get("name") or "Connected" + else: + title = "Connect Your Jibo" + self.jibo_title.setText(title) + + self.robot_settings_button.setEnabled(connected) + self.robot_action_combo.setEnabled(connected) + self.connect_button.setText("Disconnect" if connected else "Connect") + + dot_color = "#2ecc71" if connected else ("#e67e22" if host else "#bdc3c7") + _set_dot(self.conn_dot, dot_color) + _set_dot(self.status_dot, dot_color) + + if connected: + self.conn_text.setText("Connected") + else: + self.conn_text.setText("Disconnected" if host else "No IP") + + if connected: + self.status_text.setText(f"Connected via SSH to {host}" if host else "Connected via SSH") + elif host: + self.status_text.setText(f"Disconnected ({host})") + else: + self.status_text.setText("No Jibo IP configured") + + # Image swap + assets = Path(__file__).resolve().parent / "Assets" / "Jibo" + img_path = assets / ("JiboFaceForward.png" if visual_connected else "NoJiboConnected.png") + pm = QPixmap(str(img_path)) + if not pm.isNull(): + pm = pm.scaled( + self.jibo_image.size(), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + self.jibo_image.setPixmap(pm) + + def _launch_installer(self) -> None: + program, prefix = resolve_python_invocation() + subprocess.Popen( + [program, *prefix, "-m", "gui.installer_gui"], + cwd=str(Path(__file__).resolve().parents[1]), + ) + + def _launch_updater(self) -> None: + program, prefix = resolve_python_invocation() + subprocess.Popen( + [program, *prefix, "-m", "gui.updater_gui"], + cwd=str(Path(__file__).resolve().parents[1]), + ) + + def _disconnect(self) -> None: + try: + if self._ssh_client is not None: + self._ssh_client.close() + finally: + self._ssh_client = None + self._identity = None + self._sync_all() + + def _toggle_connection(self) -> None: + if self.session_connected: + self._disconnect() + return + + host = self.host + if not host: + self.status_text.setText("Enter a Jibo IP address") + return + + self._connecting = True + self._sync_enabled() + self.status_text.setText(f"Connecting to {host}...") + + try: + import paramiko # type: ignore + except Exception: + self._connecting = False + self._sync_enabled() + self.status_text.setText("Paramiko not installed; install requirements") + return + + try: + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect( + hostname=host, + username="root", + password="jibo", + look_for_keys=False, + allow_agent=False, + timeout=10, + banner_timeout=10, + auth_timeout=10, + ) + + sftp = client.open_sftp() + try: + with sftp.open("/var/jibo/identity.json", "r") as f: + raw = f.read() + finally: + sftp.close() + + if isinstance(raw, bytes): + raw_text = raw.decode("utf-8", errors="replace") + else: + raw_text = str(raw) + + identity = json.loads(raw_text) + + # Success: store session. + self._ssh_client = client + self._identity = identity if isinstance(identity, dict) else None + self.status_text.setText(f"Connected via SSH to {host}") + except Exception as e: + try: + client.close() + except Exception: + pass + self.status_text.setText(f"Connect failed: {e}") + finally: + self._connecting = False + self._sync_enabled() + self._sync_all() + + def _create_connection_pill(self) -> tuple[QFrame, QLabel, QLabel]: + pill = QFrame() + pill.setObjectName("connectionPill") + + layout = QHBoxLayout(pill) + layout.setContentsMargins(10, 4, 10, 4) + layout.setSpacing(6) + + dot = QLabel() + dot.setObjectName("connDot") + dot.setFixedSize(10, 10) + + text = QLabel("No IP") + text.setObjectName("connText") + + layout.addWidget(dot) + layout.addWidget(text) + + # Keep it tight on the tab bar. + pill.setSizePolicy(pill.sizePolicy().horizontalPolicy(), pill.sizePolicy().verticalPolicy()) + pill.setMinimumHeight(28) + return pill, dot, text + + +def main() -> int: + _ = resolve_python # keep import stable (used elsewhere) + app = QApplication(sys.argv) + ctrl = MainWindowController() + ctrl.window.show() + return int(app.exec()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/gui/process_runner.py b/JiboTools/JiboTools/gui/process_runner.py similarity index 86% rename from gui/process_runner.py rename to JiboTools/JiboTools/gui/process_runner.py index 4db3ac8..6676d72 100644 --- a/gui/process_runner.py +++ b/JiboTools/JiboTools/gui/process_runner.py @@ -12,7 +12,24 @@ from typing import Optional from PySide6.QtCore import QObject, QProcess, QTimer, Signal, Slot, Property -REPO_ROOT = Path(__file__).resolve().parents[1] +def _find_repo_root(start: Path) -> Path: + """Find the outer JiboAutoMod repo root. + + The Qt Creator project lives under JiboTools/JiboTools, while the CLI tools + (jibo_updater.py, jibo_automod.py) usually live in the outer repo root. + """ + + cur = start.resolve() + for _ in range(6): + if (cur / "jibo_updater.py").exists() and (cur / "jibo_automod.py").exists(): + return cur + if cur.parent == cur: + break + cur = cur.parent + return start.resolve().parents[1] + + +REPO_ROOT = _find_repo_root(Path(__file__).resolve()) def resolve_python_invocation() -> tuple[str, list[str]]: @@ -28,13 +45,20 @@ def resolve_python_invocation() -> tuple[str, list[str]]: if venv_py.exists(): return (str(venv_py), []) + # Prefer the current interpreter when running inside a venv (e.g. Qt Creator). + try: + if sys.executable and Path(sys.executable).exists(): + return (sys.executable, []) + except Exception: + pass + if os.name == "nt" and shutil.which("py"): return ("py", ["-3"]) if shutil.which("python3"): return ("python3", []) - return (sys.executable or "python", []) + return ("python", []) def resolve_python() -> str: diff --git a/gui/terminal_helper.py b/JiboTools/JiboTools/gui/terminal_helper.py similarity index 100% rename from gui/terminal_helper.py rename to JiboTools/JiboTools/gui/terminal_helper.py diff --git a/JiboTools/JiboTools/gui/tool_runner.ui b/JiboTools/JiboTools/gui/tool_runner.ui new file mode 100644 index 0000000..d95c938 --- /dev/null +++ b/JiboTools/JiboTools/gui/tool_runner.ui @@ -0,0 +1,148 @@ + + + ToolRunnerWindow + + + + 0 + 0 + 900 + 560 + + + + Tool + + + + + 16 + + + 16 + + + 16 + + + 16 + + + 12 + + + + + 10 + + + + + Tool + + + + 12 + true + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Start + + + + + + + Open in terminal + + + + + + + + + 10 + + + + + Jibo IP (required for updater) + + + + + + + Extra arguments (optional) + + + + + + + + + true + + + + + + + 10 + + + + + Idle + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Clear log + + + + + + + + + + + + diff --git a/JiboTools/JiboTools/gui/tool_runner_window.py b/JiboTools/JiboTools/gui/tool_runner_window.py new file mode 100644 index 0000000..40bbe9b --- /dev/null +++ b/JiboTools/JiboTools/gui/tool_runner_window.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import shlex +from pathlib import Path + +from PySide6.QtCore import QObject, Slot +from PySide6.QtGui import QCloseEvent, QTextCursor +from PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QLineEdit, QPushButton, QPlainTextEdit + +from .process_runner import ProcessRunner, resolve_python_invocation +from .terminal_helper import TerminalHelper +from .ui_loader import load_ui, require_child + + +class ToolRunnerWindow(QObject): + def __init__(self, *, title: str, script: str) -> None: + super().__init__() + + self._script = script + self._is_updater = "jibo_updater.py" in script + + ui_path = Path(__file__).resolve().parent / "tool_runner.ui" + self.window = load_ui(ui_path) + if not isinstance(self.window, QMainWindow): + raise RuntimeError("tool_runner.ui must have a QMainWindow as root") + + self.window.setWindowTitle(title) + + self.runner = ProcessRunner() + self.terminal = TerminalHelper() + + self._title_label = require_child(self.window, "titleLabel", QLabel) + self._start_stop = require_child(self.window, "startStopButton", QPushButton) + self._open_terminal = require_child(self.window, "openTerminalButton", QPushButton) + self._host_field = require_child(self.window, "hostField", QLineEdit) + self._extra_args = require_child(self.window, "extraArgsField", QLineEdit) + self._log = require_child(self.window, "logEdit", QPlainTextEdit) + self._status = require_child(self.window, "statusLabel", QLabel) + self._clear = require_child(self.window, "clearLogButton", QPushButton) + + self._title_label.setText(title) + + self._host_field.setVisible(self._is_updater) + + self._start_stop.clicked.connect(self._toggle) + self._open_terminal.clicked.connect(self._open_in_terminal) + self._clear.clicked.connect(lambda: self._log.setPlainText("")) + + self.runner.outputAppended.connect(self._append_output) + self.runner.runningChanged.connect(self._sync_buttons) + self.runner.exitCodeChanged.connect(self._sync_status) + + self._sync_buttons() + self._sync_status() + + # Ensure the process is stopped when the window closes. + self.window.closeEvent = self._on_close # type: ignore[assignment] + + def show(self) -> None: + self.window.show() + + def _build_args(self) -> list[str]: + args: list[str] = [self._script] + + if self._is_updater: + host = self._host_field.text().strip() + if host: + args += ["--ip", host] + + extra = self._extra_args.text().strip() + if extra: + args += shlex.split(extra) + + return args + + @Slot() + def _toggle(self) -> None: + if self.runner.running: + self.runner.stop() + else: + program, prefix = resolve_python_invocation() + self.runner.start(program, [*prefix, *self._build_args()]) + + @Slot() + def _open_in_terminal(self) -> None: + program, prefix = resolve_python_invocation() + self.terminal.openTerminal(program, [*prefix, *self._build_args()]) + + @Slot(str) + def _append_output(self, chunk: str) -> None: + # Keep it simple: append and scroll to end. + self._log.moveCursor(QTextCursor.End) + self._log.insertPlainText(chunk) + self._log.moveCursor(QTextCursor.End) + + def _sync_buttons(self) -> None: + running = self.runner.running + self._start_stop.setText("Stop" if running else "Start") + self._open_terminal.setEnabled(not running) + + def _sync_status(self) -> None: + if self.runner.running: + self._status.setText("Running...") + return + code = self.runner.exitCode + if code >= 0: + self._status.setText(f"Exit: {code}") + else: + self._status.setText("Idle") + + def _on_close(self, event: QCloseEvent) -> None: + try: + self.runner.stop() + except Exception: + pass + event.accept() + + +def run_tool_window(*, title: str, script: str) -> int: + app = QApplication.instance() or QApplication([]) + win = ToolRunnerWindow(title=title, script=script) + win.show() + return int(app.exec()) diff --git a/JiboTools/JiboTools/gui/ui_loader.py b/JiboTools/JiboTools/gui/ui_loader.py new file mode 100644 index 0000000..a5955f7 --- /dev/null +++ b/JiboTools/JiboTools/gui/ui_loader.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TypeVar, cast + +from PySide6.QtCore import QFile +from PySide6.QtUiTools import QUiLoader + + +T = TypeVar("T") + + +def load_ui(ui_path: Path) -> object: + loader = QUiLoader() + file = QFile(str(ui_path)) + if not file.open(QFile.ReadOnly): + raise RuntimeError(f"Failed to open UI file: {ui_path}") + try: + widget = loader.load(file) + finally: + file.close() + + if widget is None: + raise RuntimeError(f"Failed to load UI: {ui_path}") + + return widget + + +def require_child(parent: object, name: str, typ: type[T]) -> T: + # Qt objects implement findChild; keep typing light. + child = parent.findChild(typ, name) # type: ignore[attr-defined] + if child is None: + raise RuntimeError(f"UI is missing required widget '{name}' ({typ.__name__})") + return cast(T, child) diff --git a/JiboTools/JiboTools/gui/ui_tool_runner.py b/JiboTools/JiboTools/gui/ui_tool_runner.py new file mode 100644 index 0000000..8bb3424 --- /dev/null +++ b/JiboTools/JiboTools/gui/ui_tool_runner.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'tool_runner.ui' +## +## Created by: Qt User Interface Compiler version 6.10.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLineEdit, + QMainWindow, QPlainTextEdit, QPushButton, QSizePolicy, + QSpacerItem, QStatusBar, QVBoxLayout, QWidget) + +class Ui_ToolRunnerWindow(object): + def setupUi(self, ToolRunnerWindow): + if not ToolRunnerWindow.objectName(): + ToolRunnerWindow.setObjectName(u"ToolRunnerWindow") + ToolRunnerWindow.resize(900, 560) + self.centralwidget = QWidget(ToolRunnerWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.rootLayout = QVBoxLayout(self.centralwidget) + self.rootLayout.setSpacing(12) + self.rootLayout.setObjectName(u"rootLayout") + self.rootLayout.setContentsMargins(16, 16, 16, 16) + self.headerLayout = QHBoxLayout() + self.headerLayout.setSpacing(10) + self.headerLayout.setObjectName(u"headerLayout") + self.titleLabel = QLabel(self.centralwidget) + self.titleLabel.setObjectName(u"titleLabel") + font = QFont() + font.setPointSize(12) + font.setBold(True) + self.titleLabel.setFont(font) + + self.headerLayout.addWidget(self.titleLabel) + + self.headerSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.headerLayout.addItem(self.headerSpacer) + + self.startStopButton = QPushButton(self.centralwidget) + self.startStopButton.setObjectName(u"startStopButton") + + self.headerLayout.addWidget(self.startStopButton) + + self.openTerminalButton = QPushButton(self.centralwidget) + self.openTerminalButton.setObjectName(u"openTerminalButton") + + self.headerLayout.addWidget(self.openTerminalButton) + + + self.rootLayout.addLayout(self.headerLayout) + + self.argsLayout = QHBoxLayout() + self.argsLayout.setSpacing(10) + self.argsLayout.setObjectName(u"argsLayout") + self.hostField = QLineEdit(self.centralwidget) + self.hostField.setObjectName(u"hostField") + + self.argsLayout.addWidget(self.hostField) + + self.extraArgsField = QLineEdit(self.centralwidget) + self.extraArgsField.setObjectName(u"extraArgsField") + + self.argsLayout.addWidget(self.extraArgsField) + + + self.rootLayout.addLayout(self.argsLayout) + + self.logEdit = QPlainTextEdit(self.centralwidget) + self.logEdit.setObjectName(u"logEdit") + self.logEdit.setReadOnly(True) + + self.rootLayout.addWidget(self.logEdit) + + self.footerLayout = QHBoxLayout() + self.footerLayout.setSpacing(10) + self.footerLayout.setObjectName(u"footerLayout") + self.statusLabel = QLabel(self.centralwidget) + self.statusLabel.setObjectName(u"statusLabel") + + self.footerLayout.addWidget(self.statusLabel) + + self.footerSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.footerLayout.addItem(self.footerSpacer) + + self.clearLogButton = QPushButton(self.centralwidget) + self.clearLogButton.setObjectName(u"clearLogButton") + + self.footerLayout.addWidget(self.clearLogButton) + + + self.rootLayout.addLayout(self.footerLayout) + + ToolRunnerWindow.setCentralWidget(self.centralwidget) + self.statusbar = QStatusBar(ToolRunnerWindow) + self.statusbar.setObjectName(u"statusbar") + ToolRunnerWindow.setStatusBar(self.statusbar) + + self.retranslateUi(ToolRunnerWindow) + + QMetaObject.connectSlotsByName(ToolRunnerWindow) + # setupUi + + def retranslateUi(self, ToolRunnerWindow): + ToolRunnerWindow.setWindowTitle(QCoreApplication.translate("ToolRunnerWindow", u"Tool", None)) + self.titleLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Tool", None)) + self.startStopButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Start", None)) + self.openTerminalButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Open in terminal", None)) + self.hostField.setPlaceholderText(QCoreApplication.translate("ToolRunnerWindow", u"Jibo IP (required for updater)", None)) + self.extraArgsField.setPlaceholderText(QCoreApplication.translate("ToolRunnerWindow", u"Extra arguments (optional)", None)) + self.statusLabel.setText(QCoreApplication.translate("ToolRunnerWindow", u"Idle", None)) + self.clearLogButton.setText(QCoreApplication.translate("ToolRunnerWindow", u"Clear log", None)) + # retranslateUi + diff --git a/JiboTools/JiboTools/gui/updater_gui.py b/JiboTools/JiboTools/gui/updater_gui.py new file mode 100644 index 0000000..15035e5 --- /dev/null +++ b/JiboTools/JiboTools/gui/updater_gui.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import sys +from PySide6.QtWidgets import QApplication + +from .tool_runner_window import ToolRunnerWindow + + +def main() -> int: + app = QApplication(sys.argv) + win = ToolRunnerWindow(title="Updater", script="jibo_updater.py") + win.show() + return int(app.exec()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/JiboTools/JiboTools/main_panel.py b/JiboTools/JiboTools/main_panel.py new file mode 100644 index 0000000..6ce11f9 --- /dev/null +++ b/JiboTools/JiboTools/main_panel.py @@ -0,0 +1,18 @@ +"""Qt Creator entrypoint. + +This project is intended to run cleanly from Qt Creator. The UI is implemented +with Qt Widgets loaded from `.ui` files at runtime (see gui/main_panel.py and +form.ui). +""" + +from __future__ import annotations + + +def main() -> int: + from gui.main_panel import main as widgets_main + + return int(widgets_main()) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/JiboTools/JiboTools/pyproject.toml b/JiboTools/JiboTools/pyproject.toml new file mode 100644 index 0000000..6563692 --- /dev/null +++ b/JiboTools/JiboTools/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "JiboTools" + +[tool.pyside6-project] +files = [ + "main_panel.py", + "form.ui", + "gui/__init__.py", + "gui/main_panel.py", + "gui/installer_gui.py", + "gui/updater_gui.py", + "gui/process_runner.py", + "gui/terminal_helper.py", + "gui/tool_runner.ui", + "gui/tool_runner_window.py", + "gui/ui_loader.py", + "gui/Assets/Jibo/JiboFaceForward.png", + "gui/Assets/Jibo/NoJiboConnected.png", +] diff --git a/JiboTools/JiboTools/requirements.txt b/JiboTools/JiboTools/requirements.txt new file mode 100644 index 0000000..c2f3583 --- /dev/null +++ b/JiboTools/JiboTools/requirements.txt @@ -0,0 +1,2 @@ +PySide6 +paramiko diff --git a/JiboTools/JiboTools/ui_form.py b/JiboTools/JiboTools/ui_form.py new file mode 100644 index 0000000..8899ea1 --- /dev/null +++ b/JiboTools/JiboTools/ui_form.py @@ -0,0 +1,444 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'form.ui' +## +## Created by: Qt User Interface Compiler version 6.10.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout, + QFrame, QGroupBox, QHBoxLayout, QLabel, + QLineEdit, QMainWindow, QPushButton, QScrollArea, + QSizePolicy, QSpacerItem, QStatusBar, QTabWidget, + QVBoxLayout, QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(782, 649) + MainWindow.setTabShape(QTabWidget.TabShape.Rounded) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.rootLayout = QVBoxLayout(self.centralwidget) + self.rootLayout.setSpacing(12) + self.rootLayout.setObjectName(u"rootLayout") + self.rootLayout.setContentsMargins(14, 14, 14, 14) + self.tabWidget = QTabWidget(self.centralwidget) + self.tabWidget.setObjectName(u"tabWidget") + self.tabWidget.setDocumentMode(True) + self.tabWidget.setTabsClosable(False) + self.tabWidget.setMovable(False) + self.tabJibo = QWidget() + self.tabJibo.setObjectName(u"tabJibo") + self.jiboPageLayout = QHBoxLayout(self.tabJibo) + self.jiboPageLayout.setSpacing(14) + self.jiboPageLayout.setObjectName(u"jiboPageLayout") + self.configFrame = QFrame(self.tabJibo) + self.configFrame.setObjectName(u"configFrame") + self.configFrame.setMinimumSize(QSize(420, 0)) + self.configFrame.setFrameShape(QFrame.Shape.StyledPanel) + self.configFrame.setFrameShadow(QFrame.Shadow.Raised) + self.configLayout = QVBoxLayout(self.configFrame) + self.configLayout.setSpacing(10) + self.configLayout.setObjectName(u"configLayout") + self.configLayout.setContentsMargins(18, 18, 18, 18) + self.configTitle = QLabel(self.configFrame) + self.configTitle.setObjectName(u"configTitle") + font = QFont() + font.setPointSize(12) + font.setBold(True) + self.configTitle.setFont(font) + + self.configLayout.addWidget(self.configTitle) + + self.configScroll = QScrollArea(self.configFrame) + self.configScroll.setObjectName(u"configScroll") + self.configScroll.setWidgetResizable(True) + self.configScrollContents = QWidget() + self.configScrollContents.setObjectName(u"configScrollContents") + self.configScrollContents.setGeometry(QRect(0, 0, 380, 478)) + self.configScrollLayout = QVBoxLayout(self.configScrollContents) + self.configScrollLayout.setSpacing(14) + self.configScrollLayout.setObjectName(u"configScrollLayout") + self.groupPreview = QGroupBox(self.configScrollContents) + self.groupPreview.setObjectName(u"groupPreview") + self.formPreview = QFormLayout(self.groupPreview) + self.formPreview.setObjectName(u"formPreview") + self.labelOverride = QLabel(self.groupPreview) + self.labelOverride.setObjectName(u"labelOverride") + + self.formPreview.setWidget(0, QFormLayout.ItemRole.LabelRole, self.labelOverride) + + self.overrideCheck = QCheckBox(self.groupPreview) + self.overrideCheck.setObjectName(u"overrideCheck") + self.overrideCheck.setChecked(True) + + self.formPreview.setWidget(0, QFormLayout.ItemRole.FieldRole, self.overrideCheck) + + self.labelPreviewConnected = QLabel(self.groupPreview) + self.labelPreviewConnected.setObjectName(u"labelPreviewConnected") + + self.formPreview.setWidget(1, QFormLayout.ItemRole.LabelRole, self.labelPreviewConnected) + + self.previewConnectedCheck = QCheckBox(self.groupPreview) + self.previewConnectedCheck.setObjectName(u"previewConnectedCheck") + + self.formPreview.setWidget(1, QFormLayout.ItemRole.FieldRole, self.previewConnectedCheck) + + + self.configScrollLayout.addWidget(self.groupPreview) + + self.groupHomeAssistant = QGroupBox(self.configScrollContents) + self.groupHomeAssistant.setObjectName(u"groupHomeAssistant") + self.formHomeAssistant = QFormLayout(self.groupHomeAssistant) + self.formHomeAssistant.setObjectName(u"formHomeAssistant") + self.labelHaEnable = QLabel(self.groupHomeAssistant) + self.labelHaEnable.setObjectName(u"labelHaEnable") + + self.formHomeAssistant.setWidget(0, QFormLayout.ItemRole.LabelRole, self.labelHaEnable) + + self.haEnableCheck = QCheckBox(self.groupHomeAssistant) + self.haEnableCheck.setObjectName(u"haEnableCheck") + + self.formHomeAssistant.setWidget(0, QFormLayout.ItemRole.FieldRole, self.haEnableCheck) + + self.labelHaServerIp = QLabel(self.groupHomeAssistant) + self.labelHaServerIp.setObjectName(u"labelHaServerIp") + + self.formHomeAssistant.setWidget(1, QFormLayout.ItemRole.LabelRole, self.labelHaServerIp) + + self.haServerIpField = QLineEdit(self.groupHomeAssistant) + self.haServerIpField.setObjectName(u"haServerIpField") + + self.formHomeAssistant.setWidget(1, QFormLayout.ItemRole.FieldRole, self.haServerIpField) + + + self.configScrollLayout.addWidget(self.groupHomeAssistant) + + self.groupAiProvider = QGroupBox(self.configScrollContents) + self.groupAiProvider.setObjectName(u"groupAiProvider") + self.formAiProvider = QFormLayout(self.groupAiProvider) + self.formAiProvider.setObjectName(u"formAiProvider") + self.labelAiEnable = QLabel(self.groupAiProvider) + self.labelAiEnable.setObjectName(u"labelAiEnable") + + self.formAiProvider.setWidget(0, QFormLayout.ItemRole.LabelRole, self.labelAiEnable) + + self.aiEnableCheck = QCheckBox(self.groupAiProvider) + self.aiEnableCheck.setObjectName(u"aiEnableCheck") + + self.formAiProvider.setWidget(0, QFormLayout.ItemRole.FieldRole, self.aiEnableCheck) + + self.labelAiProvider = QLabel(self.groupAiProvider) + self.labelAiProvider.setObjectName(u"labelAiProvider") + + self.formAiProvider.setWidget(1, QFormLayout.ItemRole.LabelRole, self.labelAiProvider) + + self.aiProviderCombo = QComboBox(self.groupAiProvider) + self.aiProviderCombo.setObjectName(u"aiProviderCombo") + + self.formAiProvider.setWidget(1, QFormLayout.ItemRole.FieldRole, self.aiProviderCombo) + + self.labelAiEndpoint = QLabel(self.groupAiProvider) + self.labelAiEndpoint.setObjectName(u"labelAiEndpoint") + + self.formAiProvider.setWidget(2, QFormLayout.ItemRole.LabelRole, self.labelAiEndpoint) + + self.aiEndpointField = QLineEdit(self.groupAiProvider) + self.aiEndpointField.setObjectName(u"aiEndpointField") + + self.formAiProvider.setWidget(2, QFormLayout.ItemRole.FieldRole, self.aiEndpointField) + + self.labelAiKey = QLabel(self.groupAiProvider) + self.labelAiKey.setObjectName(u"labelAiKey") + + self.formAiProvider.setWidget(3, QFormLayout.ItemRole.LabelRole, self.labelAiKey) + + self.aiKeyField = QLineEdit(self.groupAiProvider) + self.aiKeyField.setObjectName(u"aiKeyField") + self.aiKeyField.setEchoMode(QLineEdit.EchoMode.Password) + + self.formAiProvider.setWidget(3, QFormLayout.ItemRole.FieldRole, self.aiKeyField) + + self.labelTokens = QLabel(self.groupAiProvider) + self.labelTokens.setObjectName(u"labelTokens") + + self.formAiProvider.setWidget(4, QFormLayout.ItemRole.LabelRole, self.labelTokens) + + self.tokensUsedLabel = QLabel(self.groupAiProvider) + self.tokensUsedLabel.setObjectName(u"tokensUsedLabel") + + self.formAiProvider.setWidget(4, QFormLayout.ItemRole.FieldRole, self.tokensUsedLabel) + + + self.configScrollLayout.addWidget(self.groupAiProvider) + + self.configBottomSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.configScrollLayout.addItem(self.configBottomSpacer) + + self.configScroll.setWidget(self.configScrollContents) + + self.configLayout.addWidget(self.configScroll) + + + self.jiboPageLayout.addWidget(self.configFrame) + + self.jiboCardFrame = QFrame(self.tabJibo) + self.jiboCardFrame.setObjectName(u"jiboCardFrame") + self.jiboCardFrame.setFrameShape(QFrame.Shape.StyledPanel) + self.jiboCardFrame.setFrameShadow(QFrame.Shadow.Raised) + self.jiboCardLayout = QVBoxLayout(self.jiboCardFrame) + self.jiboCardLayout.setSpacing(12) + self.jiboCardLayout.setObjectName(u"jiboCardLayout") + self.jiboCardLayout.setContentsMargins(18, 18, 18, 18) + self.IpConfig = QHBoxLayout() + self.IpConfig.setObjectName(u"IpConfig") + self.TryToConnect = QPushButton(self.jiboCardFrame) + self.TryToConnect.setObjectName(u"TryToConnect") + self.TryToConnect.setCheckable(False) + + self.IpConfig.addWidget(self.TryToConnect, 0, Qt.AlignmentFlag.AlignRight) + + self.JiboIpField = QLineEdit(self.jiboCardFrame) + self.JiboIpField.setObjectName(u"JiboIpField") + + self.IpConfig.addWidget(self.JiboIpField) + + + self.jiboCardLayout.addLayout(self.IpConfig) + + self.jiboHeaderLayout = QHBoxLayout() + self.jiboHeaderLayout.setObjectName(u"jiboHeaderLayout") + self.jiboTitle = QLabel(self.jiboCardFrame) + self.jiboTitle.setObjectName(u"jiboTitle") + font1 = QFont() + font1.setPointSize(12) + font1.setBold(True) + font1.setUnderline(False) + self.jiboTitle.setFont(font1) + self.jiboTitle.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.jiboHeaderLayout.addWidget(self.jiboTitle) + + + self.jiboCardLayout.addLayout(self.jiboHeaderLayout) + + self.jiboTopSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.jiboCardLayout.addItem(self.jiboTopSpacer) + + self.jiboImage = QLabel(self.jiboCardFrame) + self.jiboImage.setObjectName(u"jiboImage") + self.jiboImage.setMinimumSize(QSize(260, 260)) + self.jiboImage.setMaximumSize(QSize(260, 260)) + self.jiboImage.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.jiboCardLayout.addWidget(self.jiboImage) + + self.jiboBottomSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + + self.jiboCardLayout.addItem(self.jiboBottomSpacer) + + self.RobotSettings = QPushButton(self.jiboCardFrame) + self.RobotSettings.setObjectName(u"RobotSettings") + self.RobotSettings.setEnabled(False) + self.RobotSettings.setFlat(False) + + self.jiboCardLayout.addWidget(self.RobotSettings) + + self.comboBox = QComboBox(self.jiboCardFrame) + self.comboBox.setObjectName(u"comboBox") + self.comboBox.setEnabled(False) + self.comboBox.setEditable(True) + + self.jiboCardLayout.addWidget(self.comboBox) + + + self.jiboPageLayout.addWidget(self.jiboCardFrame) + + self.tabWidget.addTab(self.tabJibo, "") + self.tabUpdate = QWidget() + self.tabUpdate.setObjectName(u"tabUpdate") + self.updatePageLayout = QVBoxLayout(self.tabUpdate) + self.updatePageLayout.setSpacing(12) + self.updatePageLayout.setObjectName(u"updatePageLayout") + self.updateFrame = QFrame(self.tabUpdate) + self.updateFrame.setObjectName(u"updateFrame") + self.updateFrame.setFrameShape(QFrame.Shape.StyledPanel) + self.updateFrame.setFrameShadow(QFrame.Shadow.Raised) + self.updateFrameLayout = QVBoxLayout(self.updateFrame) + self.updateFrameLayout.setSpacing(12) + self.updateFrameLayout.setObjectName(u"updateFrameLayout") + self.updateFrameLayout.setContentsMargins(18, 18, 18, 18) + self.updateInfoText = QLabel(self.updateFrame) + self.updateInfoText.setObjectName(u"updateInfoText") + self.updateInfoText.setWordWrap(True) + + self.updateFrameLayout.addWidget(self.updateInfoText) + + self.updateSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.updateFrameLayout.addItem(self.updateSpacer) + + self.updateButtonsLayout = QHBoxLayout() + self.updateButtonsLayout.setSpacing(12) + self.updateButtonsLayout.setObjectName(u"updateButtonsLayout") + self.installButton = QPushButton(self.updateFrame) + self.installButton.setObjectName(u"installButton") + + self.updateButtonsLayout.addWidget(self.installButton) + + self.checkUpdatesButton = QPushButton(self.updateFrame) + self.checkUpdatesButton.setObjectName(u"checkUpdatesButton") + + self.updateButtonsLayout.addWidget(self.checkUpdatesButton) + + + self.updateFrameLayout.addLayout(self.updateButtonsLayout) + + + self.updatePageLayout.addWidget(self.updateFrame) + + self.tabWidget.addTab(self.tabUpdate, "") + self.tabSkills = QWidget() + self.tabSkills.setObjectName(u"tabSkills") + self.skillsLayout = QVBoxLayout(self.tabSkills) + self.skillsLayout.setObjectName(u"skillsLayout") + self.skillsComingSoon = QLabel(self.tabSkills) + self.skillsComingSoon.setObjectName(u"skillsComingSoon") + + self.skillsLayout.addWidget(self.skillsComingSoon) + + self.tabWidget.addTab(self.tabSkills, "") + self.tabSsh = QWidget() + self.tabSsh.setObjectName(u"tabSsh") + self.sshLayout = QVBoxLayout(self.tabSsh) + self.sshLayout.setObjectName(u"sshLayout") + self.sshComingSoon = QLabel(self.tabSsh) + self.sshComingSoon.setObjectName(u"sshComingSoon") + + self.sshLayout.addWidget(self.sshComingSoon) + + self.tabWidget.addTab(self.tabSsh, "") + self.tabFtp = QWidget() + self.tabFtp.setObjectName(u"tabFtp") + self.ftpLayout = QVBoxLayout(self.tabFtp) + self.ftpLayout.setObjectName(u"ftpLayout") + self.ftpComingSoon = QLabel(self.tabFtp) + self.ftpComingSoon.setObjectName(u"ftpComingSoon") + + self.ftpLayout.addWidget(self.ftpComingSoon) + + self.tabWidget.addTab(self.tabFtp, "") + self.tabStatus = QWidget() + self.tabStatus.setObjectName(u"tabStatus") + self.statusLayout = QVBoxLayout(self.tabStatus) + self.statusLayout.setSpacing(10) + self.statusLayout.setObjectName(u"statusLayout") + self.statusRow = QHBoxLayout() + self.statusRow.setSpacing(10) + self.statusRow.setObjectName(u"statusRow") + self.statusDot = QLabel(self.tabStatus) + self.statusDot.setObjectName(u"statusDot") + self.statusDot.setMinimumSize(QSize(10, 10)) + self.statusDot.setMaximumSize(QSize(10, 10)) + + self.statusRow.addWidget(self.statusDot) + + self.statusText = QLabel(self.tabStatus) + self.statusText.setObjectName(u"statusText") + self.statusText.setWordWrap(True) + + self.statusRow.addWidget(self.statusText) + + + self.statusLayout.addLayout(self.statusRow) + + self.tabWidget.addTab(self.tabStatus, "") + self.tabRobotOs = QWidget() + self.tabRobotOs.setObjectName(u"tabRobotOs") + self.robotOsLayout = QVBoxLayout(self.tabRobotOs) + self.robotOsLayout.setObjectName(u"robotOsLayout") + self.robotOsComingSoon = QLabel(self.tabRobotOs) + self.robotOsComingSoon.setObjectName(u"robotOsComingSoon") + + self.robotOsLayout.addWidget(self.robotOsComingSoon) + + self.tabWidget.addTab(self.tabRobotOs, "") + + self.rootLayout.addWidget(self.tabWidget) + + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + self.statusbar.setSizeGripEnabled(False) + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + + self.tabWidget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Jibo Tools", None)) + self.configTitle.setText(QCoreApplication.translate("MainWindow", u"Config", None)) + self.groupPreview.setTitle(QCoreApplication.translate("MainWindow", u"Preview", None)) + self.labelOverride.setText(QCoreApplication.translate("MainWindow", u"Override", None)) + self.overrideCheck.setText("") + self.labelPreviewConnected.setText(QCoreApplication.translate("MainWindow", u"Connected", None)) + self.previewConnectedCheck.setText("") + self.groupHomeAssistant.setTitle(QCoreApplication.translate("MainWindow", u"Home Assistant", None)) + self.labelHaEnable.setText(QCoreApplication.translate("MainWindow", u"Enabled", None)) + self.haEnableCheck.setText("") + self.labelHaServerIp.setText(QCoreApplication.translate("MainWindow", u"Server IP", None)) + self.haServerIpField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Home Assistant host", None)) + self.groupAiProvider.setTitle(QCoreApplication.translate("MainWindow", u"AI Provider", None)) + self.labelAiEnable.setText(QCoreApplication.translate("MainWindow", u"Enabled", None)) + self.aiEnableCheck.setText("") + self.labelAiProvider.setText(QCoreApplication.translate("MainWindow", u"Provider", None)) + self.labelAiEndpoint.setText(QCoreApplication.translate("MainWindow", u"API endpoint", None)) + self.aiEndpointField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"http://...", None)) + self.labelAiKey.setText(QCoreApplication.translate("MainWindow", u"API key", None)) + self.labelTokens.setText(QCoreApplication.translate("MainWindow", u"Tokens used", None)) + self.tokensUsedLabel.setText(QCoreApplication.translate("MainWindow", u"-1", None)) + self.TryToConnect.setText(QCoreApplication.translate("MainWindow", u"Connect", None)) + self.JiboIpField.setInputMask("") + self.JiboIpField.setText("") + self.JiboIpField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"e.g 192.168.1.54", None)) + self.jiboTitle.setText(QCoreApplication.translate("MainWindow", u"Connect Your Jibo", None)) + self.jiboImage.setText("") + self.RobotSettings.setText(QCoreApplication.translate("MainWindow", u"Robot Settings", None)) + self.comboBox.setCurrentText(QCoreApplication.translate("MainWindow", u"Reboot", None)) + self.comboBox.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Reboot", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabJibo), QCoreApplication.translate("MainWindow", u"Jibo", None)) + self.updateInfoText.setText(QCoreApplication.translate("MainWindow", u"Installer and updater remain available via CLI. Use the buttons below to launch their GUIs.", None)) + self.installButton.setText(QCoreApplication.translate("MainWindow", u"Install", None)) + self.checkUpdatesButton.setText(QCoreApplication.translate("MainWindow", u"Check for updates", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabUpdate), QCoreApplication.translate("MainWindow", u"Update", None)) + self.skillsComingSoon.setText(QCoreApplication.translate("MainWindow", u"Coming soon.", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabSkills), QCoreApplication.translate("MainWindow", u"Skills", None)) + self.sshComingSoon.setText(QCoreApplication.translate("MainWindow", u"Coming soon.", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabSsh), QCoreApplication.translate("MainWindow", u"SSH", None)) + self.ftpComingSoon.setText(QCoreApplication.translate("MainWindow", u"Coming soon.", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabFtp), QCoreApplication.translate("MainWindow", u"FTP", None)) + self.statusDot.setText("") + self.statusText.setText(QCoreApplication.translate("MainWindow", u"No Jibo IP configured", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabStatus), QCoreApplication.translate("MainWindow", u"Status", None)) + self.robotOsComingSoon.setText(QCoreApplication.translate("MainWindow", u"Coming soon.", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabRobotOs), QCoreApplication.translate("MainWindow", u"Robot OS", None)) + # retranslateUi + diff --git a/gui/__pycache__/__init__.cpython-313.pyc b/gui/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 55f94b3..0000000 Binary files a/gui/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/gui/__pycache__/installer_gui.cpython-313.pyc b/gui/__pycache__/installer_gui.cpython-313.pyc deleted file mode 100644 index a808dae..0000000 Binary files a/gui/__pycache__/installer_gui.cpython-313.pyc and /dev/null differ diff --git a/gui/__pycache__/main_panel.cpython-313.pyc b/gui/__pycache__/main_panel.cpython-313.pyc deleted file mode 100644 index 8c53627..0000000 Binary files a/gui/__pycache__/main_panel.cpython-313.pyc and /dev/null differ diff --git a/gui/__pycache__/process_runner.cpython-313.pyc b/gui/__pycache__/process_runner.cpython-313.pyc deleted file mode 100644 index cd7c1d7..0000000 Binary files a/gui/__pycache__/process_runner.cpython-313.pyc and /dev/null differ diff --git a/gui/__pycache__/terminal_helper.cpython-313.pyc b/gui/__pycache__/terminal_helper.cpython-313.pyc deleted file mode 100644 index ccac640..0000000 Binary files a/gui/__pycache__/terminal_helper.cpython-313.pyc and /dev/null differ diff --git a/gui/__pycache__/updater_gui.cpython-313.pyc b/gui/__pycache__/updater_gui.cpython-313.pyc deleted file mode 100644 index 0639224..0000000 Binary files a/gui/__pycache__/updater_gui.cpython-313.pyc and /dev/null differ diff --git a/gui/installer_gui.py b/gui/installer_gui.py deleted file mode 100644 index 87f0d9d..0000000 --- a/gui/installer_gui.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -from PySide6.QtCore import QUrl -from PySide6.QtGui import QGuiApplication -from PySide6.QtQml import QQmlApplicationEngine - -from .process_runner import ProcessRunner, resolve_python -from .terminal_helper import TerminalHelper - - -def main() -> int: - app = QGuiApplication(sys.argv) - - engine = QQmlApplicationEngine() - - runner = ProcessRunner() - terminal = TerminalHelper() - engine.rootContext().setContextProperty("runner", runner) - engine.rootContext().setContextProperty("terminal", terminal) - engine.rootContext().setContextProperty("pyExec", resolve_python()) - engine.rootContext().setContextProperty("toolScript", "jibo_automod.py") - engine.rootContext().setContextProperty("toolTitle", "Installer") - - qml_path = Path(__file__).resolve().parent / "qml" / "ToolRunner.qml" - engine.load(QUrl.fromLocalFile(str(qml_path))) - - if not engine.rootObjects(): - return 1 - - return app.exec() - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/gui/main_panel.py b/gui/main_panel.py deleted file mode 100644 index af4aaec..0000000 --- a/gui/main_panel.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -from PySide6.QtCore import QUrl, QObject, Slot -from PySide6.QtGui import QGuiApplication -from PySide6.QtQml import QQmlApplicationEngine - -from .process_runner import ConnectionMonitor, resolve_python, resolve_python_invocation - - -class Launcher(QObject): - def __init__(self, python_program: str, python_prefix: list[str]) -> None: - super().__init__() - self._python_program = python_program - self._python_prefix = list(python_prefix) - - @Slot() - def launchInstaller(self) -> None: - # Start installer GUI in a separate process. - import subprocess - subprocess.Popen( - [self._python_program, *self._python_prefix, "-m", "gui.installer_gui"], - cwd=str(Path(__file__).resolve().parents[1]), - ) - - @Slot() - def launchUpdater(self) -> None: - import subprocess - subprocess.Popen( - [self._python_program, *self._python_prefix, "-m", "gui.updater_gui"], - cwd=str(Path(__file__).resolve().parents[1]), - ) - - -def main() -> int: - app = QGuiApplication(sys.argv) - - engine = QQmlApplicationEngine() - - conn = ConnectionMonitor() - py_program, py_prefix = resolve_python_invocation() - py_exec = resolve_python() - engine.rootContext().setContextProperty("conn", conn) - engine.rootContext().setContextProperty("pyExec", py_exec) - engine.rootContext().setContextProperty("launcher", Launcher(py_program, py_prefix)) - - qml_path = Path(__file__).resolve().parent / "qml" / "MainPanel.qml" - engine.load(QUrl.fromLocalFile(str(qml_path))) - - if not engine.rootObjects(): - return 1 - - return app.exec() - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/gui/qml/MainPanel.qml b/gui/qml/MainPanel.qml deleted file mode 100644 index 0f2dd7b..0000000 --- a/gui/qml/MainPanel.qml +++ /dev/null @@ -1,168 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Window - -ApplicationWindow { - id: win - width: 880 - height: 520 - visible: true - title: "Jibo Tools" - - property string host: hostField.text.trim() - - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 - spacing: 14 - - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Text { - text: "Connection" - font.pixelSize: 18 - font.bold: true - } - - Rectangle { - width: 10 - height: 10 - radius: 5 - color: conn.connected ? "#2ecc71" : (host.length > 0 ? "#e67e22" : "#bdc3c7") - Layout.alignment: Qt.AlignVCenter - } - - Text { - text: conn.connected ? "SSH reachable" : (host.length > 0 ? "Not reachable" : "No IP") - color: "#555" - Layout.alignment: Qt.AlignVCenter - } - - Item { Layout.fillWidth: true } - - TextField { - id: hostField - placeholderText: "Jibo IP (e.g. 192.168.1.50)" - Layout.preferredWidth: 280 - onTextChanged: conn.host = text - } - } - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 16 - - Rectangle { - Layout.preferredWidth: 320 - Layout.fillHeight: true - radius: 14 - color: "#f6f6f6" - border.color: "#e4e4e4" - - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 - spacing: 10 - - Text { - text: "Your Jibo" - font.pixelSize: 18 - font.bold: true - } - - Item { Layout.fillHeight: true } - - Rectangle { - id: jiboCard - Layout.alignment: Qt.AlignHCenter - width: 240 - height: 240 - radius: 18 - color: "#ffffff" - border.color: "#e4e4e4" - - Image { - anchors.centerIn: parent - width: 200 - height: 200 - source: "../assets/jibo.svg" - fillMode: Image.PreserveAspectFit - } - - MouseArea { - anchors.fill: parent - onClicked: { - // Best-effort: open Chrome remote devices page. - Qt.openUrlExternally("chrome://inspect/#devices") - } - } - } - - Item { Layout.fillHeight: true } - - Text { - text: "Click Jibo to open chrome://inspect" - color: "#666" - Layout.alignment: Qt.AlignHCenter - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - radius: 14 - color: "#f6f6f6" - border.color: "#e4e4e4" - - ColumnLayout { - anchors.fill: parent - anchors.margins: 18 - spacing: 12 - - Text { - text: "Actions" - font.pixelSize: 18 - font.bold: true - } - - Text { - text: "Installer and updater remain available via CLI.\nUse the buttons below to launch their GUIs." - color: "#555" - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - Item { Layout.fillHeight: true } - - RowLayout { - Layout.fillWidth: true - spacing: 12 - - Button { - Layout.fillWidth: true - text: "Install" - enabled: true - onClicked: { - launcher.launchInstaller() - } - } - - Button { - Layout.fillWidth: true - text: "Check for updates" - enabled: true - onClicked: { - launcher.launchUpdater() - } - } - } - } - } - } - } -} diff --git a/gui/qml/ToolRunner.qml b/gui/qml/ToolRunner.qml deleted file mode 100644 index 72d3e84..0000000 --- a/gui/qml/ToolRunner.qml +++ /dev/null @@ -1,142 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -ApplicationWindow { - id: win - width: 900 - height: 560 - visible: true - title: (typeof toolTitle === "string" ? toolTitle : "Tool") - - property string script: (typeof toolScript === "string" ? toolScript : "") - property bool isUpdater: script.indexOf("jibo_updater.py") >= 0 - - function buildArgs() { - var args = [] - args.push(script) - - if (isUpdater) { - var h = hostField.text.trim() - if (h.length > 0) { - args.push("--ip") - args.push(h) - } - } - - var extra = extraArgs.text.trim() - if (extra.length > 0) { - // naive split (keeps GUI minimal) - var parts = extra.split(/\s+/) - for (var i=0; i 0) args.push(parts[i]) - } - } - - return args - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - RowLayout { - Layout.fillWidth: true - spacing: 10 - - Text { - text: title - font.pixelSize: 18 - font.bold: true - } - - Item { Layout.fillWidth: true } - - Button { - text: runner.running ? "Stop" : "Start" - onClicked: { - if (runner.running) { - runner.stop() - } else { - runner.start(pyExec, buildArgs()) - } - } - } - - Button { - text: "Open in terminal" - enabled: !runner.running - onClicked: { - terminal.openTerminal(pyExec, buildArgs()) - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 10 - - TextField { - id: hostField - visible: isUpdater - placeholderText: "Jibo IP (required for updater)" - Layout.preferredWidth: 260 - } - - TextField { - id: extraArgs - placeholderText: "Extra arguments (optional)" - Layout.fillWidth: true - } - } - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - radius: 12 - color: "#0f0f0f" - - ScrollView { - anchors.fill: parent - anchors.margins: 10 - clip: true - - TextArea { - id: log - readOnly: true - wrapMode: TextArea.Wrap - color: "#e8e8e8" - font.family: "monospace" - background: null - text: "" - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 10 - - Text { - text: runner.running ? "Running..." : (runner.exitCode >= 0 ? ("Exit: " + runner.exitCode) : "Idle") - color: "#666" - } - - Item { Layout.fillWidth: true } - - Button { - text: "Clear log" - onClicked: log.text = "" - } - } - } - - Connections { - target: runner - function onOutputAppended(chunk) { - log.text += chunk - log.cursorPosition = log.length - } - } -} diff --git a/gui/updater_gui.py b/gui/updater_gui.py deleted file mode 100644 index 9e52e5d..0000000 --- a/gui/updater_gui.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -from PySide6.QtCore import QUrl -from PySide6.QtGui import QGuiApplication -from PySide6.QtQml import QQmlApplicationEngine - -from .process_runner import ProcessRunner, resolve_python -from .terminal_helper import TerminalHelper - - -def main() -> int: - app = QGuiApplication(sys.argv) - - engine = QQmlApplicationEngine() - - runner = ProcessRunner() - terminal = TerminalHelper() - engine.rootContext().setContextProperty("runner", runner) - engine.rootContext().setContextProperty("terminal", terminal) - engine.rootContext().setContextProperty("pyExec", resolve_python()) - engine.rootContext().setContextProperty("toolScript", "jibo_updater.py") - engine.rootContext().setContextProperty("toolTitle", "Updater") - - qml_path = Path(__file__).resolve().parent / "qml" / "ToolRunner.qml" - engine.load(QUrl.fromLocalFile(str(qml_path))) - - if not engine.rootObjects(): - return 1 - - return app.exec() - - -if __name__ == "__main__": - raise SystemExit(main())