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.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/author_dialog.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Author a fragment — teacher mode.
|
|
2
|
+
|
|
3
|
+
The composer, built on what already exists: you build the winning arrangement on the real canvas,
|
|
4
|
+
this reads it into candidate objectives (by demonstration), you confirm/prune, optionally add a
|
|
5
|
+
difficulty **fork**, and finalize — which validates and writes the blessed YAML to the user content
|
|
6
|
+
layer (`~/.gini/content/fragments`). "Test" is the same Run/Check students use, so a fragment is
|
|
7
|
+
proven gradable by the real engine before it ships.
|
|
8
|
+
|
|
9
|
+
Deliberately a dialog over the live canvas rather than a new editor: the canvas IS the board, and the
|
|
10
|
+
mission engine IS the grader. We only add the authoring controls around them.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCore import Qt
|
|
15
|
+
from PySide6.QtWidgets import (
|
|
16
|
+
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QLineEdit,
|
|
17
|
+
QListWidget, QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout, QWidget,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ..domain import authoring as _au
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthorDialog(QDialog):
|
|
24
|
+
def __init__(self, parent, ctx, *, author: str = "") -> None:
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
self.ctx = ctx
|
|
27
|
+
self._author = author
|
|
28
|
+
self._objs: list[dict] = [] # core objectives (dicts)
|
|
29
|
+
self._fork_objs: list[dict] = [] # objectives on the harder fork (optional)
|
|
30
|
+
self.setWindowTitle("Author a fragment")
|
|
31
|
+
self.setMinimumWidth(560)
|
|
32
|
+
|
|
33
|
+
root = QVBoxLayout(self)
|
|
34
|
+
root.addWidget(QLabel("<b>Author a fragment from what you've built on the canvas.</b> "
|
|
35
|
+
"Build the winning arrangement, read off the objectives, then finalize.",
|
|
36
|
+
wordWrap=True))
|
|
37
|
+
|
|
38
|
+
form = QFormLayout()
|
|
39
|
+
self.fid = QLineEdit(); self.fid.setPlaceholderText("a short id, e.g. private-db-vpc")
|
|
40
|
+
form.addRow("Fragment id", self.fid)
|
|
41
|
+
self.teaches = QLineEdit(); self.teaches.setPlaceholderText("concept key, e.g. vpc-networking")
|
|
42
|
+
form.addRow("Teaches", self.teaches)
|
|
43
|
+
self.summary = QLineEdit(); self.summary.setPlaceholderText("one line the student sees")
|
|
44
|
+
form.addRow("Summary", self.summary)
|
|
45
|
+
self.spirit = QLineEdit()
|
|
46
|
+
self.spirit.setPlaceholderText("mechanism-free goal (what 'done' means, not how)")
|
|
47
|
+
form.addRow("Spirit", self.spirit)
|
|
48
|
+
root.addLayout(form)
|
|
49
|
+
|
|
50
|
+
# --- core objectives (derived from the canvas) -----------------------
|
|
51
|
+
head = QHBoxLayout()
|
|
52
|
+
head.addWidget(QLabel("<b>Objectives</b> — derived from your canvas:"))
|
|
53
|
+
head.addStretch(1)
|
|
54
|
+
derive = QPushButton("⟲ Read from canvas")
|
|
55
|
+
derive.clicked.connect(self._derive)
|
|
56
|
+
head.addWidget(derive)
|
|
57
|
+
root.addLayout(head)
|
|
58
|
+
self.obj_list = QListWidget()
|
|
59
|
+
self.obj_list.setMaximumHeight(150)
|
|
60
|
+
root.addWidget(self.obj_list)
|
|
61
|
+
rm = QPushButton("Remove selected objective"); rm.clicked.connect(self._remove_obj)
|
|
62
|
+
root.addWidget(rm)
|
|
63
|
+
|
|
64
|
+
# --- fork (optional difficulty branch) -------------------------------
|
|
65
|
+
self.fork_on = QCheckBox("Add a harder fork (the difficulty knob — earns gold)")
|
|
66
|
+
self.fork_on.toggled.connect(self._toggle_fork)
|
|
67
|
+
root.addWidget(self.fork_on)
|
|
68
|
+
self._fork_box = QWidget(); fb = QVBoxLayout(self._fork_box); fb.setContentsMargins(16, 0, 0, 0)
|
|
69
|
+
frow = QHBoxLayout()
|
|
70
|
+
self.fork_label = QLineEdit(); self.fork_label.setPlaceholderText("what the harder way is")
|
|
71
|
+
frow.addWidget(self.fork_label, 1)
|
|
72
|
+
self.fork_kind = QComboBox(); self.fork_kind.addItems(["converge", "diverge"])
|
|
73
|
+
self.fork_kind.setToolTip("converge = a harder way to the SAME goal · diverge = a different path")
|
|
74
|
+
frow.addWidget(self.fork_kind)
|
|
75
|
+
fb.addLayout(frow)
|
|
76
|
+
fhead = QHBoxLayout()
|
|
77
|
+
fhead.addWidget(QLabel("Fork objectives — build the harder solution, then:"))
|
|
78
|
+
fhead.addStretch(1)
|
|
79
|
+
fderive = QPushButton("⟲ Read fork from canvas"); fderive.clicked.connect(self._derive_fork)
|
|
80
|
+
fhead.addWidget(fderive)
|
|
81
|
+
fb.addLayout(fhead)
|
|
82
|
+
self.fork_list = QListWidget(); self.fork_list.setMaximumHeight(100)
|
|
83
|
+
fb.addWidget(self.fork_list)
|
|
84
|
+
self._fork_box.setVisible(False)
|
|
85
|
+
root.addWidget(self._fork_box)
|
|
86
|
+
|
|
87
|
+
self.note = QLabel(""); self.note.setWordWrap(True)
|
|
88
|
+
root.addWidget(self.note)
|
|
89
|
+
|
|
90
|
+
bb = QDialogButtonBox()
|
|
91
|
+
self.test_btn = bb.addButton("Test on canvas", QDialogButtonBox.ActionRole)
|
|
92
|
+
self.save_btn = bb.addButton("Finalize & save", QDialogButtonBox.AcceptRole)
|
|
93
|
+
bb.addButton("Cancel", QDialogButtonBox.RejectRole)
|
|
94
|
+
self.test_btn.clicked.connect(self._test)
|
|
95
|
+
self.save_btn.clicked.connect(self._finalize)
|
|
96
|
+
bb.rejected.connect(self.reject)
|
|
97
|
+
root.addWidget(bb)
|
|
98
|
+
|
|
99
|
+
# -- objective derivation ------------------------------------------------ #
|
|
100
|
+
def _derive(self) -> None:
|
|
101
|
+
self._objs = _au.derive_objectives(self.ctx.topology)
|
|
102
|
+
self._render(self.obj_list, self._objs)
|
|
103
|
+
if not self._objs:
|
|
104
|
+
self._warn("The canvas is empty — place the winning arrangement first, then read it.")
|
|
105
|
+
else:
|
|
106
|
+
self._info(f"Read {len(self._objs)} objective(s). Prune any you don't want to grade.")
|
|
107
|
+
|
|
108
|
+
def _derive_fork(self) -> None:
|
|
109
|
+
# the fork's objectives are whatever is on the canvas NOW that isn't already a core objective
|
|
110
|
+
core_checks = {o["check"] for o in self._objs}
|
|
111
|
+
cand = [o for o in _au.derive_objectives(self.ctx.topology) if o["check"] not in core_checks]
|
|
112
|
+
for o in cand: # keep fork objective ids distinct from core
|
|
113
|
+
o["id"] = "fork-" + o["id"]
|
|
114
|
+
self._fork_objs = cand
|
|
115
|
+
self._render(self.fork_list, self._fork_objs)
|
|
116
|
+
self._info(f"Fork: {len(cand)} objective(s) beyond the core.")
|
|
117
|
+
|
|
118
|
+
def _render(self, widget: QListWidget, objs: list[dict]) -> None:
|
|
119
|
+
widget.clear()
|
|
120
|
+
for o in objs:
|
|
121
|
+
it = QListWidgetItem(f"L{o['level']} {o['say']} · {o['check']}")
|
|
122
|
+
it.setData(Qt.UserRole, o["id"])
|
|
123
|
+
widget.addItem(it)
|
|
124
|
+
|
|
125
|
+
def _remove_obj(self) -> None:
|
|
126
|
+
row = self.obj_list.currentRow()
|
|
127
|
+
if 0 <= row < len(self._objs):
|
|
128
|
+
del self._objs[row]
|
|
129
|
+
self._render(self.obj_list, self._objs)
|
|
130
|
+
|
|
131
|
+
def _toggle_fork(self, on: bool) -> None:
|
|
132
|
+
self._fork_box.setVisible(on)
|
|
133
|
+
self.adjustSize()
|
|
134
|
+
|
|
135
|
+
# -- build the dict ------------------------------------------------------ #
|
|
136
|
+
def _dict(self) -> dict | None:
|
|
137
|
+
fid = self.fid.text().strip()
|
|
138
|
+
if not fid:
|
|
139
|
+
self._warn("Give the fragment an id."); return None
|
|
140
|
+
if not self._objs:
|
|
141
|
+
self._warn("Read at least one objective from the canvas."); return None
|
|
142
|
+
forks = None
|
|
143
|
+
if self.fork_on.isChecked() and self._fork_objs:
|
|
144
|
+
forks = [{"id": "hard", "label": self.fork_label.text().strip(),
|
|
145
|
+
"difficulty": 2, "kind": self.fork_kind.currentText(),
|
|
146
|
+
"objectives": self._fork_objs}]
|
|
147
|
+
return _au.build_fragment_dict(
|
|
148
|
+
frag_id=fid, teaches=self.teaches.text().strip(), summary=self.summary.text().strip(),
|
|
149
|
+
spirit=self.spirit.text().strip(), objectives=self._objs, forks=forks,
|
|
150
|
+
author=self._author)
|
|
151
|
+
|
|
152
|
+
def _test(self) -> None:
|
|
153
|
+
"""Validate structurally, then hand the teacher a real playtest: build the fragment, drop into
|
|
154
|
+
it as a mission, and use Run/Check. Here we just validate + confirm gradability; the caller
|
|
155
|
+
launches the live mission."""
|
|
156
|
+
d = self._dict()
|
|
157
|
+
if d is None:
|
|
158
|
+
return
|
|
159
|
+
problems = _au.validate_dict(d)
|
|
160
|
+
if problems:
|
|
161
|
+
self._warn("Not gradable yet: " + "; ".join(problems))
|
|
162
|
+
return
|
|
163
|
+
self._info("Valid + gradable. Finalize to save, or keep refining. "
|
|
164
|
+
"(Play it as a mission to prove it winnable against real Docker.)")
|
|
165
|
+
|
|
166
|
+
def _finalize(self) -> None:
|
|
167
|
+
d = self._dict()
|
|
168
|
+
if d is None:
|
|
169
|
+
return
|
|
170
|
+
try:
|
|
171
|
+
path = _au.save_fragment(d)
|
|
172
|
+
except ValueError as e:
|
|
173
|
+
self._warn(f"Can't save — not gradable: {e}")
|
|
174
|
+
return
|
|
175
|
+
from ..domain import fragments as _frag
|
|
176
|
+
_frag.reload() # pick the new fragment up immediately
|
|
177
|
+
QMessageBox.information(self, "Saved",
|
|
178
|
+
f"Fragment '{d['id']}' saved and loaded.\n\n{path}\n\n"
|
|
179
|
+
f"Upload it to the Teaching Center to compose experiments from it.")
|
|
180
|
+
self.accept()
|
|
181
|
+
|
|
182
|
+
# -- notes --------------------------------------------------------------- #
|
|
183
|
+
def _warn(self, msg): self.note.setText(msg); self.note.setStyleSheet("color:#d9534f;font-weight:600;")
|
|
184
|
+
def _info(self, msg): self.note.setText(msg); self.note.setStyleSheet("color:#3fb950;")
|
gini/ui/board_dialog.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Hardware → Set Up a Board: give a GINI32 board the lab Wi-Fi, over USB.
|
|
2
|
+
|
|
3
|
+
This is the only step in the whole GINI32 story that needs a cable, and a student
|
|
4
|
+
does it once per board. Everything the dialog writes is the minimum a board
|
|
5
|
+
cannot work out for itself; its address, gateway, hotspot and subnet all still
|
|
6
|
+
come from the canvas when it checks in.
|
|
7
|
+
|
|
8
|
+
The serial work happens on a worker thread. Talking to a board takes a couple of
|
|
9
|
+
seconds — it reboots when the port is opened — and freezing the window for that
|
|
10
|
+
long makes a working setup look like a hang.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCore import QObject, Qt, Signal
|
|
15
|
+
from PySide6.QtWidgets import (
|
|
16
|
+
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
|
|
17
|
+
QLineEdit, QMessageBox, QPushButton, QVBoxLayout, QWidget,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ..services import boardsetup as bs
|
|
21
|
+
from .worker_host import WorkerHost, _LIVE_THREADS # noqa: F401 (tests inspect it)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _note(text: str) -> QLabel:
|
|
25
|
+
lab = QLabel(text)
|
|
26
|
+
lab.setWordWrap(True)
|
|
27
|
+
lab.setStyleSheet("color: palette(mid);")
|
|
28
|
+
return lab
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Scan(QObject):
|
|
32
|
+
"""Finds boards on a worker thread (opening a port resets and waits for it)."""
|
|
33
|
+
done = Signal(list, list) # [BoardInfo], [PortInfo]
|
|
34
|
+
failed = Signal(str)
|
|
35
|
+
|
|
36
|
+
def run(self) -> None:
|
|
37
|
+
try:
|
|
38
|
+
boards, others = bs.detect_boards()
|
|
39
|
+
self.done.emit(boards, others)
|
|
40
|
+
except Exception as e: # never let a USB oddity kill the UI
|
|
41
|
+
self.failed.emit(str(e))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Apply(QObject):
|
|
45
|
+
"""Writes settings to one board, off the GUI thread."""
|
|
46
|
+
done = Signal(bool, str)
|
|
47
|
+
|
|
48
|
+
def __init__(self, port: str, ssid: str, password: str, board_id: str) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._args = (port, ssid, password, board_id)
|
|
51
|
+
|
|
52
|
+
def run(self) -> None:
|
|
53
|
+
port, ssid, password, board_id = self._args
|
|
54
|
+
try:
|
|
55
|
+
with bs.BoardConsole(port) as con:
|
|
56
|
+
if not con.wait_for_prompt():
|
|
57
|
+
self.done.emit(False, "the board never reached its prompt — unplug "
|
|
58
|
+
"it, plug it back in, and try again")
|
|
59
|
+
return
|
|
60
|
+
ok, msg = con.apply(ssid, password, board_id)
|
|
61
|
+
self.done.emit(ok, msg)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.done.emit(False, str(e))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BoardSetupDialog(WorkerHost, QDialog):
|
|
67
|
+
"""Pick a plugged-in board, confirm the details, write them."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, parent, settings, known_ids: list[str] | None = None) -> None:
|
|
70
|
+
super().__init__(parent)
|
|
71
|
+
self.setWindowTitle("Set Up a GINI32 Board")
|
|
72
|
+
self.setMinimumWidth(560)
|
|
73
|
+
self._settings = settings
|
|
74
|
+
self._known_ids = list(known_ids or [])
|
|
75
|
+
self._boards: list[bs.BoardInfo] = []
|
|
76
|
+
self._thread: QThread | None = None
|
|
77
|
+
self._worker: QObject | None = None # kept alive; see _run()
|
|
78
|
+
self._connections: list = [] # (signal, slot) pairs cut on close
|
|
79
|
+
self._alive = True # cleared on close: workers must stop touching us
|
|
80
|
+
self.applied_id = ""
|
|
81
|
+
|
|
82
|
+
root = QVBoxLayout(self)
|
|
83
|
+
root.addWidget(_note(
|
|
84
|
+
"Plug a board into this computer with a USB cable. This is the only step "
|
|
85
|
+
"that needs a cable — after it, the board works over Wi-Fi."))
|
|
86
|
+
|
|
87
|
+
form = QFormLayout()
|
|
88
|
+
row = QHBoxLayout()
|
|
89
|
+
self.board = QComboBox()
|
|
90
|
+
self.board.setMinimumWidth(320)
|
|
91
|
+
row.addWidget(self.board, 1)
|
|
92
|
+
self.rescan = QPushButton("Rescan")
|
|
93
|
+
self.rescan.clicked.connect(self.scan)
|
|
94
|
+
row.addWidget(self.rescan)
|
|
95
|
+
holder = QWidget()
|
|
96
|
+
holder.setLayout(row)
|
|
97
|
+
form.addRow("Board", holder)
|
|
98
|
+
|
|
99
|
+
self.ssid = QLineEdit(getattr(settings, "board_wifi_ssid", "") or "")
|
|
100
|
+
self.ssid.setPlaceholderText("the Wi-Fi this laptop is on — 2.4 GHz")
|
|
101
|
+
form.addRow("Lab Wi-Fi name", self.ssid)
|
|
102
|
+
self.password = QLineEdit(getattr(settings, "board_wifi_password", "") or "")
|
|
103
|
+
self.password.setEchoMode(QLineEdit.Password)
|
|
104
|
+
form.addRow("Lab Wi-Fi password", self.password)
|
|
105
|
+
self.board_id = QLineEdit()
|
|
106
|
+
self.board_id.setPlaceholderText("gini-1")
|
|
107
|
+
form.addRow("Name this board", self.board_id)
|
|
108
|
+
root.addLayout(form)
|
|
109
|
+
|
|
110
|
+
self.status = _note("")
|
|
111
|
+
root.addWidget(self.status)
|
|
112
|
+
root.addWidget(_note(
|
|
113
|
+
"The name you give the board is what you type into the GINI32 element's "
|
|
114
|
+
"BoardID on the canvas — pick something short you can read off a sticker."))
|
|
115
|
+
|
|
116
|
+
self.buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
|
|
117
|
+
self.apply_btn = self.buttons.addButton("Set Up Board",
|
|
118
|
+
QDialogButtonBox.AcceptRole)
|
|
119
|
+
self.apply_btn.clicked.connect(self._apply)
|
|
120
|
+
self.buttons.rejected.connect(self.reject)
|
|
121
|
+
root.addWidget(self.buttons)
|
|
122
|
+
|
|
123
|
+
self.board.currentIndexChanged.connect(self._board_changed)
|
|
124
|
+
self.scan()
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------- scanning
|
|
127
|
+
|
|
128
|
+
def scan(self) -> None:
|
|
129
|
+
self._busy(True, "Looking for boards…")
|
|
130
|
+
self.board.clear()
|
|
131
|
+
self._run(_Scan(), self._scanned)
|
|
132
|
+
|
|
133
|
+
def _worker_failed(self, message: str) -> None:
|
|
134
|
+
if self._alive:
|
|
135
|
+
self._busy(False, f"Could not look for boards: {message}")
|
|
136
|
+
|
|
137
|
+
def _scanned(self, boards: list, others: list) -> None:
|
|
138
|
+
if not self._alive: # the dialog was closed while we were scanning
|
|
139
|
+
return
|
|
140
|
+
self._boards = boards
|
|
141
|
+
self.board.clear()
|
|
142
|
+
for b in boards:
|
|
143
|
+
label = b.board_id or "(unnamed board)"
|
|
144
|
+
if b.configured:
|
|
145
|
+
label += f" — set up for '{b.ssid}'"
|
|
146
|
+
else:
|
|
147
|
+
label += " — not set up yet"
|
|
148
|
+
self.board.addItem(f"{label} [{b.port}]", b.port)
|
|
149
|
+
|
|
150
|
+
if boards:
|
|
151
|
+
self._busy(False, f"Found {len(boards)} board"
|
|
152
|
+
f"{'s' if len(boards) != 1 else ''}.")
|
|
153
|
+
elif others:
|
|
154
|
+
names = ", ".join(o.device for o in others[:3])
|
|
155
|
+
self._busy(False,
|
|
156
|
+
f"Serial devices found ({names}), but none answered as a GINI32 "
|
|
157
|
+
f"board. Is the gBridge firmware flashed on it?")
|
|
158
|
+
else:
|
|
159
|
+
self._busy(False,
|
|
160
|
+
"No board found. Check the cable is plugged in at both ends — "
|
|
161
|
+
"and that it is a DATA cable: many USB cables carry power only, "
|
|
162
|
+
"which looks exactly like a dead board.")
|
|
163
|
+
self._board_changed()
|
|
164
|
+
|
|
165
|
+
def _board_changed(self) -> None:
|
|
166
|
+
"""Suggest a name that does not collide with one already in use."""
|
|
167
|
+
b = self._selected()
|
|
168
|
+
if b is None:
|
|
169
|
+
return
|
|
170
|
+
if b.board_id and b.board_id not in ("gini32-1",): # keep a real, chosen name
|
|
171
|
+
self.board_id.setText(b.board_id)
|
|
172
|
+
else:
|
|
173
|
+
taken = self._known_ids + [x.board_id for x in self._boards
|
|
174
|
+
if x is not b and x.board_id]
|
|
175
|
+
self.board_id.setText(bs.suggest_board_id(taken))
|
|
176
|
+
if b.ssid and not self.ssid.text().strip():
|
|
177
|
+
self.ssid.setText(b.ssid)
|
|
178
|
+
|
|
179
|
+
def _selected(self) -> bs.BoardInfo | None:
|
|
180
|
+
port = self.board.currentData()
|
|
181
|
+
return next((b for b in self._boards if b.port == port), None)
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------- applying
|
|
184
|
+
|
|
185
|
+
def _apply(self) -> None:
|
|
186
|
+
b = self._selected()
|
|
187
|
+
if b is None:
|
|
188
|
+
QMessageBox.information(self, "No board selected",
|
|
189
|
+
"Plug a board in and press Rescan.")
|
|
190
|
+
return
|
|
191
|
+
ssid = self.ssid.text().strip()
|
|
192
|
+
pw = self.password.text()
|
|
193
|
+
bid = self.board_id.text().strip()
|
|
194
|
+
if not ssid:
|
|
195
|
+
QMessageBox.warning(self, "Wi-Fi name needed",
|
|
196
|
+
"Enter the name of the Wi-Fi network this laptop is on. "
|
|
197
|
+
"The board joins it to reach gBuilder.")
|
|
198
|
+
return
|
|
199
|
+
if not bid:
|
|
200
|
+
QMessageBox.warning(self, "Name needed",
|
|
201
|
+
"Give the board a short name — it is how the canvas "
|
|
202
|
+
"refers to it.")
|
|
203
|
+
return
|
|
204
|
+
self._busy(True, f"Setting up '{bid}'…")
|
|
205
|
+
self._run(_Apply(b.port, ssid, pw, bid), self._applied)
|
|
206
|
+
|
|
207
|
+
def _applied(self, ok: bool, msg: str) -> None:
|
|
208
|
+
if not self._alive:
|
|
209
|
+
return
|
|
210
|
+
self._busy(False, "")
|
|
211
|
+
if not ok:
|
|
212
|
+
QMessageBox.warning(self, "Setup failed", msg)
|
|
213
|
+
return
|
|
214
|
+
# Remember the lab Wi-Fi so the next board is a single click.
|
|
215
|
+
try:
|
|
216
|
+
self._settings.board_wifi_ssid = self.ssid.text().strip()
|
|
217
|
+
self._settings.board_wifi_password = self.password.text()
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
self.applied_id = self.board_id.text().strip()
|
|
221
|
+
QMessageBox.information(
|
|
222
|
+
self, "Board is set up",
|
|
223
|
+
f"{msg}.\n\nThe board is restarting and will join the Wi-Fi on its own. "
|
|
224
|
+
f"You can unplug it now — it only needs power from here on.\n\n"
|
|
225
|
+
f"Use '{self.applied_id}' as the BoardID on the canvas.")
|
|
226
|
+
self.accept()
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------- utils
|
|
229
|
+
|
|
230
|
+
# _run() and _detach() now come from WorkerHost — one copy of the threading
|
|
231
|
+
# rules, shared with the flash dialog. See ui/worker_host.py.
|
|
232
|
+
|
|
233
|
+
def closeEvent(self, event) -> None:
|
|
234
|
+
self._detach()
|
|
235
|
+
super().closeEvent(event)
|
|
236
|
+
|
|
237
|
+
def reject(self) -> None:
|
|
238
|
+
self._detach()
|
|
239
|
+
super().reject()
|
|
240
|
+
|
|
241
|
+
def _busy(self, busy: bool, message: str) -> None:
|
|
242
|
+
if not self._alive:
|
|
243
|
+
return
|
|
244
|
+
self.status.setText(message)
|
|
245
|
+
self.rescan.setEnabled(not busy)
|
|
246
|
+
self.apply_btn.setEnabled(not busy)
|
|
247
|
+
self.setCursor(Qt.WaitCursor if busy else Qt.ArrowCursor)
|
gini/ui/branding.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""App branding — the GINI mascot used as the window / taskbar / dock icon."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PySide6.QtGui import QIcon
|
|
8
|
+
|
|
9
|
+
ASSETS = Path(__file__).parent / "assets"
|
|
10
|
+
APP_ICON_PNG = ASSETS / "app_icon.png"
|
|
11
|
+
APP_ICON_ICNS = ASSETS / "app_icon.icns"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def icon_path() -> str:
|
|
15
|
+
return str(APP_ICON_PNG)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@lru_cache(maxsize=1)
|
|
19
|
+
def app_icon() -> QIcon:
|
|
20
|
+
"""The GINI mascot as a QIcon (empty if the asset is missing — never crashes)."""
|
|
21
|
+
return QIcon(str(APP_ICON_PNG)) if APP_ICON_PNG.exists() else QIcon()
|