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,244 @@
1
+ """Hardware → Flash a Board: put gBridge firmware on a GINI32 board over USB.
2
+
3
+ This is the step that was missing, and its absence made the rest of the Hardware menu
4
+ untrue. *Set Up a Board* talks to the board's `gini> ` console — which only exists once
5
+ firmware is on it. So a student holding a brand-new board could not begin in gBuilder at
6
+ all; they needed a terminal, ESP-IDF and the CLI. Now the menu covers a board's whole
7
+ life: flash it, set it up, reset it, list them.
8
+
9
+ Flashing does NOT erase the board. The images are written at their own offsets, which
10
+ leaves NVS at 0x9000 alone, so a board that was already set up keeps its id, its lab
11
+ Wi-Fi and its LED pin across a firmware update. See services/boardflash.py.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from PySide6.QtCore import QObject, Qt, Signal
16
+ from PySide6.QtWidgets import (
17
+ QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
18
+ QMessageBox, QProgressBar, QPushButton, QVBoxLayout,
19
+ )
20
+
21
+ from ..services import boardflash as bf
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 _Detect(QObject):
34
+ """Enumerate ports and ask the selected one what chip it is."""
35
+ done = Signal(list, str) # [PortInfo], chip target ("" = unknown)
36
+ failed = Signal(str)
37
+
38
+ def __init__(self, port: str = "") -> None:
39
+ super().__init__()
40
+ self._port = port
41
+
42
+ def run(self) -> None:
43
+ try:
44
+ ports = bs.list_ports()
45
+ chip = bf.detect_chip(self._port) if self._port else ""
46
+ self.done.emit(ports, chip)
47
+ except Exception as e: # a USB oddity must never kill the UI
48
+ self.failed.emit(str(e))
49
+
50
+
51
+ class _Flash(QObject):
52
+ """Write the images, off the GUI thread — this takes ten to thirty seconds."""
53
+ done = Signal(bool, str)
54
+ progress = Signal(str)
55
+
56
+ def __init__(self, port: str, firmware) -> None:
57
+ super().__init__()
58
+ self._port, self._fw = port, firmware
59
+
60
+ def run(self) -> None:
61
+ try:
62
+ res = bf.flash(self._port, self._fw, on_progress=self.progress.emit)
63
+ self.done.emit(res.ok, res.message)
64
+ except Exception as e:
65
+ self.done.emit(False, str(e))
66
+
67
+
68
+ class FlashBoardDialog(WorkerHost, QDialog):
69
+ """Pick a port, confirm the chip, write the firmware."""
70
+
71
+ def __init__(self, parent=None) -> None:
72
+ super().__init__(parent)
73
+ self.setWindowTitle("Flash a Board")
74
+ self.setMinimumWidth(560)
75
+ self._alive = True
76
+ self._thread = None
77
+ self._worker = None
78
+ self._connections = []
79
+ self.flashed_port = "" # read by the caller to chain into Set Up
80
+
81
+ root = QVBoxLayout(self)
82
+ root.addWidget(_note(
83
+ "Puts the gBridge firmware on a new board. You only need this once per "
84
+ "board — afterwards use <b>Set Up a Board</b> to give it the lab Wi-Fi.<br><br>"
85
+ "Re-flashing an existing board is safe: its id, Wi-Fi and pairing are stored "
86
+ "in a separate area of flash that is not touched."))
87
+
88
+ form = QFormLayout()
89
+ self.port = QComboBox()
90
+ row = QHBoxLayout()
91
+ row.addWidget(self.port, 1)
92
+ self.rescan = QPushButton("Rescan")
93
+ self.rescan.clicked.connect(self.scan)
94
+ row.addWidget(self.rescan)
95
+ form.addRow("Board on", row)
96
+
97
+ self.chip = QLabel("—")
98
+ form.addRow("Chip", self.chip)
99
+ self.firmware = QLabel("—")
100
+ form.addRow("Firmware", self.firmware)
101
+ root.addLayout(form)
102
+
103
+ self.bar = QProgressBar()
104
+ self.bar.setRange(0, 0) # indeterminate: esptool's own % is not parsed
105
+ self.bar.setVisible(False)
106
+ root.addWidget(self.bar)
107
+
108
+ self.status = _note("")
109
+ root.addWidget(self.status)
110
+
111
+ self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
112
+ self.flash_btn = QPushButton("Flash")
113
+ self.flash_btn.setDefault(True)
114
+ self.buttons.addButton(self.flash_btn, QDialogButtonBox.AcceptRole)
115
+ self.flash_btn.clicked.connect(self._flash)
116
+ self.buttons.rejected.connect(self.reject)
117
+ root.addWidget(self.buttons)
118
+
119
+ self.port.currentIndexChanged.connect(self._port_changed)
120
+ self.scan()
121
+
122
+ # ---------------------------------------------------------------- scanning
123
+
124
+ def scan(self) -> None:
125
+ if not bf.esptool_available():
126
+ # Say this once, plainly, instead of letting every flash fail with a
127
+ # traceback. esptool is a pure-Python package — no toolchain — so the fix
128
+ # really is a single pip install.
129
+ self._busy(False, "")
130
+ self.status.setText(
131
+ "<b>esptool is not installed.</b> It is a small Python package and "
132
+ "needs no toolchain:<br><tt>pip install esptool</tt>")
133
+ self.flash_btn.setEnabled(False)
134
+ return
135
+ self._busy(True, "looking for boards…")
136
+ self._run(_Detect(), self._scanned)
137
+
138
+ def _scanned(self, ports: list, chip: str) -> None:
139
+ if not self._alive:
140
+ return
141
+ self.port.blockSignals(True)
142
+ self.port.clear()
143
+ for p in ports:
144
+ self.port.addItem(p.label, p.device)
145
+ self.port.blockSignals(False)
146
+ if not ports:
147
+ self._busy(False, "")
148
+ self.status.setText(
149
+ "No board found. Plug one in with a <b>data</b> USB cable — charge-only "
150
+ "cables carry no data lines and look exactly like a dead board.")
151
+ self.flash_btn.setEnabled(False)
152
+ return
153
+ self._busy(False, "")
154
+ self._port_changed()
155
+
156
+ def _port_changed(self) -> None:
157
+ """Identify the chip, then show which firmware would be written to it."""
158
+ if not self._alive or self.port.count() == 0:
159
+ return
160
+ self._busy(True, "asking the board what it is…")
161
+ self._run(_Detect(self.port.currentData()), self._identified)
162
+
163
+ def _identified(self, _ports: list, chip: str) -> None:
164
+ if not self._alive:
165
+ return
166
+ self._busy(False, "")
167
+ if not chip:
168
+ self.chip.setText("could not tell")
169
+ self.firmware.setText("—")
170
+ self.status.setText(
171
+ "The board did not answer. Hold <b>BOOT</b>, tap <b>RESET</b>, release "
172
+ "BOOT, then press Rescan — some boards cannot be reset over USB alone.")
173
+ self.flash_btn.setEnabled(False)
174
+ return
175
+ self.chip.setText(chip)
176
+ fw = bf.available(chip)
177
+ if fw is None:
178
+ have = ", ".join(bf.available_targets()) or "none"
179
+ self.firmware.setText("not available")
180
+ self.status.setText(
181
+ f"No firmware is shipped for <b>{chip}</b> (available: {have}). "
182
+ f"Build one with <tt>GINI32_TARGET={chip} ./gini32 build</tt>.")
183
+ self.flash_btn.setEnabled(False)
184
+ return
185
+ self._fw = fw
186
+ self.firmware.setText(f"{fw.build or 'gbridge'} · {fw.total_bytes // 1024} KB")
187
+ self.status.setText("")
188
+ self.flash_btn.setEnabled(True)
189
+
190
+ # ---------------------------------------------------------------- flashing
191
+
192
+ def _flash(self) -> None:
193
+ fw = getattr(self, "_fw", None)
194
+ if fw is None or self.port.count() == 0:
195
+ return
196
+ port = self.port.currentData()
197
+ self.bar.setVisible(True)
198
+ self._busy(True, "flashing — do not unplug the board…")
199
+ self.flashed_port = port
200
+ self._run(_Flash(port, fw), self._flashed)
201
+
202
+ def _worker_progress(self, message: str) -> None:
203
+ if self._alive:
204
+ self.status.setText(message)
205
+
206
+ def _flashed(self, ok: bool, msg: str) -> None:
207
+ if not self._alive:
208
+ return
209
+ self.bar.setVisible(False)
210
+ self._busy(False, "")
211
+ if not ok:
212
+ self.flashed_port = ""
213
+ QMessageBox.warning(self, "Could not flash the board", msg)
214
+ self.status.setText(msg)
215
+ return
216
+ QMessageBox.information(
217
+ self, "Board flashed",
218
+ f"{msg}.\n\nNext: use Set Up a Board to give it the lab Wi-Fi and an id.")
219
+ self.accept()
220
+
221
+ def _worker_failed(self, message: str) -> None:
222
+ if not self._alive:
223
+ return
224
+ self.bar.setVisible(False)
225
+ self._busy(False, "")
226
+ self.status.setText(message)
227
+
228
+ # ------------------------------------------------------------------- utils
229
+
230
+ def closeEvent(self, event) -> None:
231
+ self._detach()
232
+ super().closeEvent(event)
233
+
234
+ def reject(self) -> None:
235
+ self._detach()
236
+ super().reject()
237
+
238
+ def _busy(self, busy: bool, message: str) -> None:
239
+ if not self._alive:
240
+ return
241
+ self.status.setText(message)
242
+ self.rescan.setEnabled(not busy)
243
+ self.flash_btn.setEnabled(not busy)
244
+ self.setCursor(Qt.WaitCursor if busy else Qt.ArrowCursor)
gini/ui/flow_layout.py ADDED
@@ -0,0 +1,63 @@
1
+ """A wrapping flow layout — lays widgets left-to-right, wrapping to the next line when the
2
+ row is full (a tag/word-cloud arrangement). Standard Qt pattern, kept minimal."""
3
+ from __future__ import annotations
4
+
5
+ from PySide6.QtCore import QPoint, QRect, QSize, Qt
6
+ from PySide6.QtWidgets import QLayout, QSizePolicy
7
+
8
+
9
+ class FlowLayout(QLayout):
10
+ def __init__(self, parent=None, margin=0, spacing=6):
11
+ super().__init__(parent)
12
+ self._items = []
13
+ self.setContentsMargins(margin, margin, margin, margin)
14
+ self.setSpacing(spacing)
15
+
16
+ def addItem(self, item):
17
+ self._items.append(item)
18
+
19
+ def count(self):
20
+ return len(self._items)
21
+
22
+ def itemAt(self, i):
23
+ return self._items[i] if 0 <= i < len(self._items) else None
24
+
25
+ def takeAt(self, i):
26
+ return self._items.pop(i) if 0 <= i < len(self._items) else None
27
+
28
+ def expandingDirections(self):
29
+ return Qt.Orientations(Qt.Orientation(0))
30
+
31
+ def hasHeightForWidth(self):
32
+ return True
33
+
34
+ def heightForWidth(self, width):
35
+ return self._do_layout(QRect(0, 0, width, 0), True)
36
+
37
+ def setGeometry(self, rect):
38
+ super().setGeometry(rect)
39
+ self._do_layout(rect, False)
40
+
41
+ def sizeHint(self):
42
+ return self.minimumSize()
43
+
44
+ def minimumSize(self):
45
+ size = QSize()
46
+ for it in self._items:
47
+ size = size.expandedTo(it.minimumSize())
48
+ m = self.contentsMargins()
49
+ return size + QSize(m.left() + m.right(), m.top() + m.bottom())
50
+
51
+ def _do_layout(self, rect, test_only):
52
+ x, y, line_h = rect.x(), rect.y(), 0
53
+ sp = self.spacing()
54
+ for it in self._items:
55
+ hint = it.sizeHint()
56
+ w, h = hint.width(), hint.height()
57
+ if x + w > rect.right() and line_h > 0: # wrap to next row
58
+ x = rect.x(); y = y + line_h + sp; line_h = 0
59
+ if not test_only:
60
+ it.setGeometry(QRect(QPoint(x, y), hint))
61
+ x += w + sp
62
+ line_h = max(line_h, h)
63
+ return y + line_h - rect.y()