gini-toolkit 6.0.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,130 @@
1
+ """Live process tree for the Machine Lab — the process hierarchy as a tree, not a flat table.
2
+
3
+ Built from each process's parent link (ppid, from gini_dump). fork() adds a child, exec() relabels,
4
+ exit() shows a zombie, wait() reaps it, and an exiting parent's children re-parent to init — all
5
+ visible as the tree changes. State is colour-coded; the running process(es) are highlighted; user
6
+ processes get a kill affordance. Driven entirely by the live serial dump (no gdb).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from PySide6.QtCore import Qt, Signal
11
+ from PySide6.QtGui import QColor, QFont
12
+ from PySide6.QtWidgets import QHeaderView, QPushButton, QTreeWidget, QTreeWidgetItem
13
+
14
+ from ..domain.xv6 import build_process_tree
15
+ from .theme import ThemeManager
16
+ from .theme.manager import scale_css as _scss
17
+
18
+ _STATE_ACCENT = {"running": "green", "runnable": "amber", "sleeping": "slate",
19
+ "zombie": "red", "used": "slate", "unused": "slate"}
20
+
21
+
22
+ class ProcessTree(QTreeWidget):
23
+ """A QTreeWidget rendering of the process hierarchy. `kill_requested(pid)` fires when the
24
+ user clicks a process's kill affordance (never shown for init/sh)."""
25
+
26
+ kill_requested = Signal(int)
27
+
28
+ def __init__(self, theme: ThemeManager) -> None:
29
+ super().__init__()
30
+ self.theme = theme
31
+ self._live = False
32
+ self._killing: set = set() # pids with a kill in flight -> show a pending state
33
+ self.setColumnCount(4)
34
+ self.setHeaderLabels(["process", "pid", "state", ""])
35
+ self.setRootIsDecorated(True)
36
+ self.setIndentation(16)
37
+ self.setSelectionMode(QTreeWidget.NoSelection)
38
+ self.setFocusPolicy(Qt.NoFocus)
39
+ self.setColumnWidth(1, 48)
40
+ self.setColumnWidth(3, 84)
41
+ self.header().setSectionResizeMode(0, QHeaderView.Stretch)
42
+ t = theme.theme
43
+ self.setStyleSheet(
44
+ f"QTreeWidget{{background:{t.panel};color:{t.text};border:none;font-size:12px;"
45
+ f"outline:none;}}"
46
+ f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
47
+ "padding:4px;}"
48
+ f"QTreeWidget::item{{padding:2px 0;}}")
49
+
50
+ def set_live(self, live: bool) -> None:
51
+ self._live = live
52
+
53
+ def set_procs(self, procs, running_pids=(), flags=None) -> None:
54
+ """Rebuild the tree from the current process list. `running_pids` are highlighted; `flags`
55
+ maps pid -> a short reason string (e.g. 'starving') that badges the process."""
56
+ t = self.theme.theme
57
+ running = set(running_pids or [])
58
+ flags = flags or {}
59
+ self._killing &= {p.pid for p in procs} # drop pending marks for procs that are now gone
60
+ self.clear()
61
+ items: dict = {} # pid -> QTreeWidgetItem
62
+ for node in build_process_tree(procs):
63
+ p = node.proc
64
+ parent_item = items.get(p.parent)
65
+ it = QTreeWidgetItem(parent_item if parent_item is not None else self)
66
+ it.setText(0, p.name)
67
+ it.setText(1, str(p.pid))
68
+ it.setText(2, p.state)
69
+ color = QColor(t.accent_for(_STATE_ACCENT.get(p.state, "slate")))
70
+ it.setForeground(2, color)
71
+ if p.pid in running:
72
+ it.setForeground(0, color)
73
+ f = it.font(0); f.setBold(True); it.setFont(0, f)
74
+ elif p.state == "zombie":
75
+ for c in (0, 1, 2):
76
+ it.setForeground(c, QColor(t.faint))
77
+ if p.pid in flags: # scheduling badge (e.g. starvation)
78
+ reason = flags[p.pid]
79
+ it.setText(2, f"{p.state} ⚠ {reason}")
80
+ it.setForeground(2, QColor(t.accent_for("red")))
81
+ it.setToolTip(2, reason)
82
+ items[p.pid] = it
83
+ if self._live and p.pid > 2: # kill affordance (never init/sh)
84
+ self.setItemWidget(it, 3, self._kill_button(p.pid))
85
+ self.expandAll()
86
+
87
+ def _kill_button(self, pid: int) -> QPushButton:
88
+ """A proper labelled Kill button (the old bare ✕ was easy to miss), with a persistent
89
+ 'killing…' pending state so the click visibly registers even while the kill lands."""
90
+ t = self.theme.theme
91
+ pending = pid in self._killing
92
+ red = t.accent_for("red")
93
+ b = QPushButton("killing…" if pending else "Kill")
94
+ b.setFixedHeight(20)
95
+ b.setToolTip(f"kill pid {pid}")
96
+ b.setCursor(Qt.PointingHandCursor)
97
+ if pending:
98
+ b.setEnabled(False)
99
+ b.setStyleSheet(_scss(
100
+ f"QPushButton{{color:{t.faint};background:transparent;border:1px solid {t.line};"
101
+ "border-radius:5px;padding:0 8px;font-size:11px;}"))
102
+ else:
103
+ b.setStyleSheet(_scss(
104
+ f"QPushButton{{color:{red};background:transparent;border:1px solid {red};"
105
+ "border-radius:5px;padding:0 8px;font-size:11px;font-weight:600;}"
106
+ f"QPushButton:hover{{color:#ffffff;background:{red};}}"))
107
+ b.clicked.connect(lambda _c=False, p=pid: self._on_kill_clicked(p))
108
+ return b
109
+
110
+ def _on_kill_clicked(self, pid: int) -> None:
111
+ self._killing.add(pid) # mark pending; survives the ~0.5s tree rebuilds
112
+ it = self._find_item(pid)
113
+ if it is not None: # flip THIS button to the pending state immediately
114
+ self.setItemWidget(it, 3, self._kill_button(pid))
115
+ self.kill_requested.emit(pid)
116
+
117
+ def _find_item(self, pid: int):
118
+ def walk(it):
119
+ if it.text(1) == str(pid):
120
+ return it
121
+ for i in range(it.childCount()):
122
+ r = walk(it.child(i))
123
+ if r is not None:
124
+ return r
125
+ return None
126
+ for i in range(self.topLevelItemCount()):
127
+ r = walk(self.topLevelItem(i))
128
+ if r is not None:
129
+ return r
130
+ return None
@@ -0,0 +1,179 @@
1
+ """Hardware → Reset a Board: release a board's pairing, over USB.
2
+
3
+ A claimed board answers only the laptop that owns it and is invisible to every other
4
+ gBuilder. That is what stops thirty laptops in a room from stealing each other's
5
+ hardware — but it means a board claimed by the wrong laptop looks, from the student's
6
+ side, exactly like a board that is simply broken: it powers up, joins the Wi-Fi, and
7
+ never appears on anyone's canvas.
8
+
9
+ There is deliberately no timed auto-release: a board must never re-open itself while its
10
+ owner is at lunch. Physical possession is the authority instead — and USB *is* what
11
+ physical possession means in software. Hence this dialog: whoever is holding the board
12
+ can always take it back, and nobody else can.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from PySide6.QtCore import QObject, Qt, Signal
17
+ from PySide6.QtWidgets import (
18
+ QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
19
+ QMessageBox, QPushButton, QVBoxLayout,
20
+ )
21
+
22
+ from ..services import boardsetup as bs
23
+ from .worker_host import WorkerHost
24
+
25
+
26
+ def _note(text: str) -> QLabel:
27
+ lab = QLabel(text)
28
+ lab.setWordWrap(True)
29
+ lab.setStyleSheet("color: palette(mid);")
30
+ return lab
31
+
32
+
33
+ class _Scan(QObject):
34
+ done = Signal(list, list)
35
+ failed = Signal(str)
36
+
37
+ def run(self) -> None:
38
+ try:
39
+ boards, others = bs.detect_boards()
40
+ self.done.emit(boards, others)
41
+ except Exception as e:
42
+ self.failed.emit(str(e))
43
+
44
+
45
+ class _Unpair(QObject):
46
+ done = Signal(bool, str)
47
+
48
+ def __init__(self, port: str) -> None:
49
+ super().__init__()
50
+ self._port = port
51
+
52
+ def run(self) -> None:
53
+ try:
54
+ with bs.BoardConsole(self._port) as con:
55
+ if not con.wait_for_prompt():
56
+ self.done.emit(False, "the board never reached its prompt — unplug "
57
+ "it, plug it back in, and try again")
58
+ return
59
+ ok, msg = con.unpair()
60
+ self.done.emit(ok, msg)
61
+ except Exception as e:
62
+ self.done.emit(False, str(e))
63
+
64
+
65
+ class ResetBoardDialog(WorkerHost, QDialog):
66
+ """Pick a plugged-in board and release whoever owns it."""
67
+
68
+ def __init__(self, parent=None) -> None:
69
+ super().__init__(parent)
70
+ self.setWindowTitle("Reset a Board")
71
+ self.setMinimumWidth(560)
72
+ self._alive = True
73
+ self._thread = None
74
+ self._worker = None
75
+ self._connections = []
76
+
77
+ root = QVBoxLayout(self)
78
+ root.addWidget(_note(
79
+ "Releases a board's pairing, so any gBuilder can claim it again.<br><br>"
80
+ "Use this when a board is online and healthy but never appears on the "
81
+ "canvas — that usually means another laptop claimed it. Settings, id and "
82
+ "Wi-Fi are kept; only the pairing is cleared."))
83
+
84
+ form = QFormLayout()
85
+ self.board = QComboBox()
86
+ row = QHBoxLayout()
87
+ row.addWidget(self.board, 1)
88
+ self.rescan = QPushButton("Rescan")
89
+ self.rescan.clicked.connect(self.scan)
90
+ row.addWidget(self.rescan)
91
+ form.addRow("Board", row)
92
+ self.owner = QLabel("—")
93
+ form.addRow("Claimed by", self.owner)
94
+ root.addLayout(form)
95
+
96
+ self.status = _note("")
97
+ root.addWidget(self.status)
98
+
99
+ self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
100
+ self.reset_btn = QPushButton("Release")
101
+ self.reset_btn.setDefault(True)
102
+ self.buttons.addButton(self.reset_btn, QDialogButtonBox.AcceptRole)
103
+ self.reset_btn.clicked.connect(self._reset)
104
+ self.buttons.rejected.connect(self.reject)
105
+ root.addWidget(self.buttons)
106
+
107
+ self.board.currentIndexChanged.connect(self._board_changed)
108
+ self.scan()
109
+
110
+ def scan(self) -> None:
111
+ self._busy(True, "looking for boards…")
112
+ self._run(_Scan(), self._scanned)
113
+
114
+ def _scanned(self, boards: list, others: list) -> None:
115
+ if not self._alive:
116
+ return
117
+ self._boards = boards
118
+ self.board.blockSignals(True)
119
+ self.board.clear()
120
+ for b in boards:
121
+ self.board.addItem(f"{b.board_id or '(no id)'} — {b.port}", b.port)
122
+ self.board.blockSignals(False)
123
+ self._busy(False, "")
124
+ if not boards:
125
+ self.owner.setText("—")
126
+ self.reset_btn.setEnabled(False)
127
+ self.status.setText(
128
+ "No board found. Plug one in with a <b>data</b> USB cable."
129
+ + (f" ({len(others)} other serial device(s) seen.)" if others else ""))
130
+ return
131
+ self._board_changed()
132
+
133
+ def _board_changed(self) -> None:
134
+ if not self._alive or self.board.currentIndex() < 0:
135
+ return
136
+ b = self._boards[self.board.currentIndex()]
137
+ owner = (getattr(b, "owner", "") or "").strip()
138
+ self.owner.setText(owner or "not claimed")
139
+ # Releasing an unclaimed board is harmless, so it stays enabled: the point is to
140
+ # let someone who suspects a stuck claim just try it and see.
141
+ self.reset_btn.setEnabled(True)
142
+
143
+ def _reset(self) -> None:
144
+ if self.board.currentIndex() < 0:
145
+ return
146
+ self._busy(True, "releasing…")
147
+ self._run(_Unpair(self.board.currentData()), self._done)
148
+
149
+ def _done(self, ok: bool, msg: str) -> None:
150
+ if not self._alive:
151
+ return
152
+ self._busy(False, "")
153
+ if not ok:
154
+ QMessageBox.warning(self, "Could not release the board", msg)
155
+ self.status.setText(msg)
156
+ return
157
+ QMessageBox.information(self, "Board released", msg)
158
+ self.accept()
159
+
160
+ def _worker_failed(self, message: str) -> None:
161
+ if self._alive:
162
+ self._busy(False, "")
163
+ self.status.setText(message)
164
+
165
+ def closeEvent(self, event) -> None:
166
+ self._detach()
167
+ super().closeEvent(event)
168
+
169
+ def reject(self) -> None:
170
+ self._detach()
171
+ super().reject()
172
+
173
+ def _busy(self, busy: bool, message: str) -> None:
174
+ if not self._alive:
175
+ return
176
+ self.status.setText(message)
177
+ self.rescan.setEnabled(not busy)
178
+ self.reset_btn.setEnabled(not busy)
179
+ self.setCursor(Qt.WaitCursor if busy else Qt.ArrowCursor)