tinyGuiUpdate , Updating script v1 finnished

This commit is contained in:
2026-03-17 00:08:41 +02:00
parent 2f07910512
commit 8dfb15ac40
29 changed files with 1881 additions and 98 deletions

0
gui/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

13
gui/assets/jibo.svg Normal file
View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f2f2f2"/>
<stop offset="1" stop-color="#d9d9d9"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="256" height="256" fill="none"/>
<circle cx="128" cy="128" r="110" fill="url(#g)" stroke="#9a9a9a" stroke-width="6"/>
<circle cx="128" cy="128" r="70" fill="#ffffff" stroke="#b3b3b3" stroke-width="6"/>
<rect x="88" y="168" width="80" height="26" rx="13" fill="#c7c7c7"/>
<text x="128" y="128" font-family="sans-serif" font-size="28" text-anchor="middle" fill="#444">Jibo</text>
</svg>

After

Width:  |  Height:  |  Size: 696 B

37
gui/installer_gui.py Normal file
View File

@@ -0,0 +1,37 @@
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())

59
gui/main_panel.py Normal file
View File

@@ -0,0 +1,59 @@
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())

185
gui/process_runner.py Normal file
View File

@@ -0,0 +1,185 @@
from __future__ import annotations
import os
import shlex
import shutil
import socket
import subprocess
import sys
from pathlib import Path
from typing import Optional
from PySide6.QtCore import QObject, QProcess, QTimer, Signal, Slot, Property
REPO_ROOT = Path(__file__).resolve().parents[1]
def resolve_python_invocation() -> tuple[str, list[str]]:
"""Return (program, prefix_args) to invoke Python reliably.
On Windows, prefer the repo-local venv; otherwise prefer the `py -3` launcher
when present so we dont depend on `python.exe` being on PATH.
"""
venv_py = REPO_ROOT / ".venv" / ("Scripts" if os.name == "nt" else "bin") / (
"python.exe" if os.name == "nt" else "python"
)
if venv_py.exists():
return (str(venv_py), [])
if os.name == "nt" and shutil.which("py"):
return ("py", ["-3"])
if shutil.which("python3"):
return ("python3", [])
return (sys.executable or "python", [])
def resolve_python() -> str:
program, prefix = resolve_python_invocation()
if prefix:
# Best-effort string representation (mostly for display)
return " ".join([program] + prefix)
return program
def can_connect(host: str, port: int, timeout_s: float = 0.8) -> bool:
try:
with socket.create_connection((host, port), timeout=timeout_s):
return True
except OSError:
return False
def _pick_terminal_command() -> Optional[list[str]]:
if os.name == "nt":
return None
candidates: list[list[str]] = []
# Debian/Ubuntu alternative system
candidates.append(["x-terminal-emulator", "-e"])
candidates.append(["gnome-terminal", "--"])
candidates.append(["konsole", "-e"])
candidates.append(["xfce4-terminal", "-e"])
candidates.append(["xterm", "-e"])
for cmd in candidates:
if shutil.which(cmd[0]):
return cmd
return None
def spawn_in_terminal(argv: list[str]) -> bool:
"""Best-effort external terminal launcher.
Returns True if a terminal was spawned, False otherwise.
"""
if os.name == "nt":
# Use cmd.exe window, keep it open (/k)
# Build a single string command for cmd.
cmdline = " ".join(shlex.quote(a) for a in argv)
subprocess.Popen(["cmd", "/c", "start", "cmd", "/k", cmdline], shell=False)
return True
term = _pick_terminal_command()
if not term:
return False
subprocess.Popen(term + argv, cwd=str(REPO_ROOT))
return True
class ProcessRunner(QObject):
runningChanged = Signal()
exitCodeChanged = Signal()
outputAppended = Signal(str)
def __init__(self) -> None:
super().__init__()
self._proc = QProcess(self)
self._proc.setProcessChannelMode(QProcess.MergedChannels)
self._proc.readyReadStandardOutput.connect(self._on_ready)
self._proc.finished.connect(self._on_finished)
self._exit_code: int = -1
@Property(bool, notify=runningChanged)
def running(self) -> bool:
return self._proc.state() != QProcess.NotRunning
@Property(int, notify=exitCodeChanged)
def exitCode(self) -> int:
return self._exit_code
@Slot(str, list)
def start(self, program: str, arguments: list) -> None:
if self.running:
return
self._exit_code = -1
self.exitCodeChanged.emit()
self._proc.setProgram(program)
self._proc.setArguments([str(a) for a in arguments])
self._proc.setWorkingDirectory(str(REPO_ROOT))
self._proc.start()
self.runningChanged.emit()
@Slot()
def stop(self) -> None:
if not self.running:
return
self._proc.terminate()
if not self._proc.waitForFinished(1500):
self._proc.kill()
self.runningChanged.emit()
def _on_ready(self) -> None:
data = bytes(self._proc.readAllStandardOutput()).decode("utf-8", errors="replace")
if data:
self.outputAppended.emit(data)
def _on_finished(self, exit_code: int, _status) -> None:
self._exit_code = int(exit_code)
self.exitCodeChanged.emit()
self.runningChanged.emit()
class ConnectionMonitor(QObject):
hostChanged = Signal()
connectedChanged = Signal()
def __init__(self) -> None:
super().__init__()
self._host = ""
self._connected = False
self._timer = QTimer(self)
self._timer.setInterval(1000)
self._timer.timeout.connect(self._poll)
self._timer.start()
@Property(str, notify=hostChanged)
def host(self) -> str:
return self._host
@host.setter
def host(self, value: str) -> None:
value = (value or "").strip()
if value == self._host:
return
self._host = value
self.hostChanged.emit()
self._poll()
@Property(bool, notify=connectedChanged)
def connected(self) -> bool:
return self._connected
def _poll(self) -> None:
host = self._host
connected = False
if host:
connected = can_connect(host, 22)
if connected != self._connected:
self._connected = connected
self.connectedChanged.emit()

168
gui/qml/MainPanel.qml Normal file
View File

@@ -0,0 +1,168 @@
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()
}
}
}
}
}
}
}
}

142
gui/qml/ToolRunner.qml Normal file
View File

@@ -0,0 +1,142 @@
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
}
}
}

12
gui/terminal_helper.py Normal file
View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from PySide6.QtCore import QObject, Slot
from .process_runner import spawn_in_terminal
class TerminalHelper(QObject):
@Slot(str, list, result=bool)
def openTerminal(self, program: str, arguments: list) -> bool:
argv = [program] + [str(a) for a in arguments]
return spawn_in_terminal(argv)

37
gui/updater_gui.py Normal file
View File

@@ -0,0 +1,37 @@
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())