completely redone menu now using widgets instead of QML

This commit is contained in:
2026-03-18 02:07:35 +02:00
parent 8dfb15ac40
commit c6235093e1
32 changed files with 2017 additions and 445 deletions

84
JiboTools/JiboTools/.gitignore vendored Normal file
View File

@@ -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/

643
JiboTools/JiboTools/form.ui Normal file
View File

@@ -0,0 +1,643 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>782</width>
<height>649</height>
</rect>
</property>
<property name="windowTitle">
<string>Jibo Tools</string>
</property>
<property name="tabShape">
<enum>QTabWidget::TabShape::Rounded</enum>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="rootLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>14</number>
</property>
<property name="topMargin">
<number>14</number>
</property>
<property name="rightMargin">
<number>14</number>
</property>
<property name="bottomMargin">
<number>14</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<property name="tabsClosable">
<bool>false</bool>
</property>
<property name="movable">
<bool>false</bool>
</property>
<widget class="QWidget" name="tabJibo">
<attribute name="title">
<string>Jibo</string>
</attribute>
<layout class="QHBoxLayout" name="jiboPageLayout">
<property name="spacing">
<number>14</number>
</property>
<item>
<widget class="QFrame" name="configFrame">
<property name="minimumSize">
<size>
<width>420</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="configLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>18</number>
</property>
<property name="topMargin">
<number>18</number>
</property>
<property name="rightMargin">
<number>18</number>
</property>
<property name="bottomMargin">
<number>18</number>
</property>
<item>
<widget class="QLabel" name="configTitle">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Config</string>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="configScroll">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="configScrollContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>380</width>
<height>478</height>
</rect>
</property>
<layout class="QVBoxLayout" name="configScrollLayout">
<property name="spacing">
<number>14</number>
</property>
<item>
<widget class="QGroupBox" name="groupPreview">
<property name="title">
<string>Preview</string>
</property>
<layout class="QFormLayout" name="formPreview">
<item row="0" column="0">
<widget class="QLabel" name="labelOverride">
<property name="text">
<string>Override</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="overrideCheck">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelPreviewConnected">
<property name="text">
<string>Connected</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="previewConnectedCheck">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupHomeAssistant">
<property name="title">
<string>Home Assistant</string>
</property>
<layout class="QFormLayout" name="formHomeAssistant">
<item row="0" column="0">
<widget class="QLabel" name="labelHaEnable">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="haEnableCheck">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelHaServerIp">
<property name="text">
<string>Server IP</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="haServerIpField">
<property name="placeholderText">
<string>Home Assistant host</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupAiProvider">
<property name="title">
<string>AI Provider</string>
</property>
<layout class="QFormLayout" name="formAiProvider">
<item row="0" column="0">
<widget class="QLabel" name="labelAiEnable">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="aiEnableCheck">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelAiProvider">
<property name="text">
<string>Provider</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="aiProviderCombo"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelAiEndpoint">
<property name="text">
<string>API endpoint</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="aiEndpointField">
<property name="placeholderText">
<string>http://...</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelAiKey">
<property name="text">
<string>API key</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="aiKeyField">
<property name="echoMode">
<enum>QLineEdit::EchoMode::Password</enum>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelTokens">
<property name="text">
<string>Tokens used</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="tokensUsedLabel">
<property name="text">
<string>-1</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="configBottomSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="jiboCardFrame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="jiboCardLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>18</number>
</property>
<property name="topMargin">
<number>18</number>
</property>
<property name="rightMargin">
<number>18</number>
</property>
<property name="bottomMargin">
<number>18</number>
</property>
<item>
<layout class="QHBoxLayout" name="IpConfig">
<item alignment="Qt::AlignmentFlag::AlignRight">
<widget class="QPushButton" name="TryToConnect">
<property name="text">
<string>Connect</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="JiboIpField">
<property name="inputMask">
<string/>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>e.g 192.168.1.54</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="jiboHeaderLayout">
<item>
<widget class="QLabel" name="jiboTitle">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
<underline>false</underline>
</font>
</property>
<property name="text">
<string>Connect Your Jibo</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="jiboTopSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="jiboImage">
<property name="minimumSize">
<size>
<width>260</width>
<height>260</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>260</width>
<height>260</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="jiboBottomSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="RobotSettings">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Robot Settings</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="currentText">
<string>Reboot</string>
</property>
<property name="placeholderText">
<string>Reboot</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabUpdate">
<attribute name="title">
<string>Update</string>
</attribute>
<layout class="QVBoxLayout" name="updatePageLayout">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QFrame" name="updateFrame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="updateFrameLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>18</number>
</property>
<property name="topMargin">
<number>18</number>
</property>
<property name="rightMargin">
<number>18</number>
</property>
<property name="bottomMargin">
<number>18</number>
</property>
<item>
<widget class="QLabel" name="updateInfoText">
<property name="text">
<string>Installer and updater remain available via CLI. Use the buttons below to launch their GUIs.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="updateSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="updateButtonsLayout">
<property name="spacing">
<number>12</number>
</property>
<item>
<widget class="QPushButton" name="installButton">
<property name="text">
<string>Install</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="checkUpdatesButton">
<property name="text">
<string>Check for updates</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabSkills">
<attribute name="title">
<string>Skills</string>
</attribute>
<layout class="QVBoxLayout" name="skillsLayout">
<item>
<widget class="QLabel" name="skillsComingSoon">
<property name="text">
<string>Coming soon.</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabSsh">
<attribute name="title">
<string>SSH</string>
</attribute>
<layout class="QVBoxLayout" name="sshLayout">
<item>
<widget class="QLabel" name="sshComingSoon">
<property name="text">
<string>Coming soon.</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabFtp">
<attribute name="title">
<string>FTP</string>
</attribute>
<layout class="QVBoxLayout" name="ftpLayout">
<item>
<widget class="QLabel" name="ftpComingSoon">
<property name="text">
<string>Coming soon.</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabStatus">
<attribute name="title">
<string>Status</string>
</attribute>
<layout class="QVBoxLayout" name="statusLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<layout class="QHBoxLayout" name="statusRow">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="statusDot">
<property name="minimumSize">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusText">
<property name="text">
<string>No Jibo IP configured</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabRobotOs">
<attribute name="title">
<string>Robot OS</string>
</attribute>
<layout class="QVBoxLayout" name="robotOsLayout">
<item>
<widget class="QLabel" name="robotOsComingSoon">
<property name="text">
<string>Coming soon.</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar">
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View File

@@ -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())

View File

@@ -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())

View File

@@ -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:

View File

@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ToolRunnerWindow</class>
<widget class="QMainWindow" name="ToolRunnerWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>560</height>
</rect>
</property>
<property name="windowTitle">
<string>Tool</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="rootLayout">
<property name="leftMargin">
<number>16</number>
</property>
<property name="topMargin">
<number>16</number>
</property>
<property name="rightMargin">
<number>16</number>
</property>
<property name="bottomMargin">
<number>16</number>
</property>
<property name="spacing">
<number>12</number>
</property>
<item>
<layout class="QHBoxLayout" name="headerLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="titleLabel">
<property name="text">
<string>Tool</string>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
</widget>
</item>
<item>
<spacer name="headerSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="startStopButton">
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openTerminalButton">
<property name="text">
<string>Open in terminal</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="argsLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QLineEdit" name="hostField">
<property name="placeholderText">
<string>Jibo IP (required for updater)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="extraArgsField">
<property name="placeholderText">
<string>Extra arguments (optional)</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPlainTextEdit" name="logEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="footerLayout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>Idle</string>
</property>
</widget>
</item>
<item>
<spacer name="footerSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="clearLogButton">
<property name="text">
<string>Clear log</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -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())

View File

@@ -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)

View File

@@ -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

View File

@@ -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())

View File

@@ -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())

View File

@@ -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",
]

View File

@@ -0,0 +1,2 @@
PySide6
paramiko

View File

@@ -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

View File

@@ -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())

View File

@@ -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())

View File

@@ -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()
}
}
}
}
}
}
}
}

View File

@@ -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<parts.length; i++) {
if (parts[i].length > 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
}
}
}

View File

@@ -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())