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
gini/ui/chat_panel.py ADDED
@@ -0,0 +1,7 @@
1
+ """RETIRED. Messaging is no longer a separate dock — it folded into the Ask GINI panel as one
2
+ unified conversation surface (GINI + people, one transcript, a conversation switcher). See
3
+ `assistant.py` (`_select_convo`, `_render_convo`, `_send_human`).
4
+
5
+ This module is intentionally empty; it remains only so any stale import fails loudly rather than
6
+ silently resurrecting a second chat panel.
7
+ """
gini/ui/cpu_journey.py ADDED
@@ -0,0 +1,212 @@
1
+ """CPU journey — a step-driven walkthrough of a system call (trap) vs a context switch (swtch).
2
+
3
+ Pick a mode, then Step through the stages. At each stage the view shows the privilege band
4
+ (user/kernel) and process lane the CPU is in, and highlights which save-area is touched — the
5
+ TRAPFRAME (all user registers, on a trap) or the CONTEXT (14 callee-saved, on a swtch). Real
6
+ register values from the running process seed the syscall path.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from PySide6.QtCore import Qt
11
+ from PySide6.QtWidgets import (
12
+ QButtonGroup, QDialog, QFrame, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,
13
+ )
14
+
15
+ from ..domain.cpu_journey import JOURNEY_TITLES, JOURNEYS
16
+ from .theme import ThemeManager, icons
17
+
18
+
19
+ class CpuJourney(QDialog):
20
+ def __init__(self, parent, theme: ThemeManager, device=None, cpu=None, frame=None) -> None:
21
+ super().__init__(parent)
22
+ self.theme = theme
23
+ self.device = device
24
+ self.cpu = cpu # a CpuState (real regs) to seed the syscall path
25
+ self.frame = frame # a TrapFrame from /trapcatch (a real frozen trap)
26
+ self._mode = "syscall"
27
+ self._i = 0
28
+
29
+ t = theme.theme
30
+ self.setWindowTitle(f"CPU journey — {getattr(device, 'name', 'xv6')}")
31
+ self.resize(820, 500)
32
+ self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
33
+ root = QVBoxLayout(self)
34
+
35
+ head = QLabel("A system call is a TRAP (same process, user↔kernel, saves the trapframe). "
36
+ "A context switch is swtch (a different process, kernel↔kernel, saves the "
37
+ "context). Preemption is both. Step through and watch which save-area moves.")
38
+ head.setWordWrap(True); head.setStyleSheet(f"color:{t.muted};font-size:12px;")
39
+ root.addWidget(head)
40
+
41
+ # a live banner when we froze a real trap (Phase 2): its actual scause/sepc/stval
42
+ self._live = QLabel(); self._live.setWordWrap(True)
43
+ self._live.setVisible(bool(frame))
44
+ if frame is not None and getattr(frame, "ok", False):
45
+ acc = t.accent_for("amber")
46
+ self._live.setStyleSheet(
47
+ f"color:{acc};background:{t.panel2};border:1px solid {acc};border-radius:8px;"
48
+ "padding:6px 10px;font-family:monospace;font-size:12px;")
49
+ msg = f"● froze a live trap: {frame.kind_name} · scause {frame.scause} · sepc {frame.sepc}"
50
+ if frame.stval and frame.stval not in ("0x0", "0x0000000000000000"):
51
+ msg += f" · stval {frame.stval}"
52
+ if frame.pid is not None and frame.pid >= 0:
53
+ msg += f" · pid {frame.pid}"
54
+ self._live.setText(msg)
55
+ elif frame is not None:
56
+ self._live.setStyleSheet(f"color:{t.faint};font-size:12px;")
57
+ self._live.setText("● couldn't freeze a trap (kernel idle) — showing the reference "
58
+ "walkthrough. Launch a program and try again.")
59
+ root.addWidget(self._live)
60
+
61
+ modes = QHBoxLayout()
62
+ self._mode_group = QButtonGroup(self); self._mode_group.setExclusive(True)
63
+ self._mode_btns = {}
64
+ for key in ("syscall", "context", "preempt"):
65
+ b = QPushButton(JOURNEY_TITLES[key])
66
+ b.setCheckable(True); b.setChecked(key == "syscall")
67
+ b.setStyleSheet(self._btn_css())
68
+ b.clicked.connect(lambda _c=False, k=key: self._set_mode(k))
69
+ self._mode_group.addButton(b); self._mode_btns[key] = b
70
+ modes.addWidget(b)
71
+ modes.addStretch(1)
72
+ root.addLayout(modes)
73
+
74
+ self._stage_row = QHBoxLayout(); self._stage_row.setSpacing(4)
75
+ sw = QWidget(); sw.setLayout(self._stage_row)
76
+ root.addWidget(sw)
77
+
78
+ self._band = QLabel(); self._band.setStyleSheet(f"color:{t.muted};font-size:12px;")
79
+ root.addWidget(self._band)
80
+
81
+ self._caption = QLabel(); self._caption.setWordWrap(True)
82
+ self._caption.setStyleSheet(
83
+ f"color:{t.text};font-size:13px;background:{t.panel2};border:1px solid {t.line};"
84
+ "border-radius:10px;padding:12px;")
85
+ self._caption.setMinimumHeight(90)
86
+ root.addWidget(self._caption, 1)
87
+
88
+ save = QHBoxLayout()
89
+ self._tf = self._save_card("trapframe", "34 user registers · saved on every TRAP")
90
+ self._ctx = self._save_card("context", "14 callee-saved · saved on every swtch")
91
+ save.addWidget(self._tf); save.addWidget(self._ctx)
92
+ root.addLayout(save)
93
+
94
+ nav = QHBoxLayout()
95
+ self._prev = QPushButton(" Prev"); self._prev.setStyleSheet(self._btn_css())
96
+ self._prev.clicked.connect(lambda: self._step(-1))
97
+ self._next = QPushButton(" Step ▸"); self._next.setStyleSheet(self._btn_css())
98
+ self._next.clicked.connect(lambda: self._step(1))
99
+ self._pos = QLabel(); self._pos.setStyleSheet(f"color:{t.muted};font-size:12px;")
100
+ nav.addWidget(self._pos); nav.addStretch(1)
101
+ nav.addWidget(self._prev); nav.addWidget(self._next)
102
+ root.addLayout(nav)
103
+
104
+ self._set_mode("syscall")
105
+
106
+ def _btn_css(self) -> str:
107
+ t = self.theme.theme
108
+ return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
109
+ f"border-radius:8px;padding:6px 12px;}}"
110
+ f"QPushButton:checked{{border-color:{t.accent};color:{t.accent_for('blue')};}}"
111
+ f"QPushButton:hover{{border-color:{t.accent};}}"
112
+ f"QPushButton:disabled{{color:{t.faint};}}")
113
+
114
+ def _save_card(self, name, sub) -> QFrame:
115
+ t = self.theme.theme
116
+ f = QFrame(); f.setObjectName(name)
117
+ v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 8)
118
+ title = QLabel(name); title.setObjectName("t")
119
+ sublbl = QLabel(sub); sublbl.setObjectName("s")
120
+ v.addWidget(title); v.addWidget(sublbl)
121
+ f._title, f._sub = title, sublbl
122
+ return f
123
+
124
+ def _set_mode(self, key) -> None:
125
+ self._mode = key
126
+ self._i = 0
127
+ for k, b in self._mode_btns.items():
128
+ b.setChecked(k == key)
129
+ # rebuild the stage chips
130
+ while self._stage_row.count():
131
+ w = self._stage_row.takeAt(0).widget()
132
+ if w:
133
+ w.deleteLater()
134
+ self._chips = []
135
+ t = self.theme.theme
136
+ for s in JOURNEYS[key]:
137
+ c = QLabel(s.title)
138
+ c.setAlignment(Qt.AlignCenter)
139
+ c.setStyleSheet(
140
+ f"color:{t.muted};background:{t.panel2};border:1px solid {t.line};"
141
+ "border-radius:6px;padding:4px 6px;font-size:11px;")
142
+ self._chips.append(c)
143
+ self._stage_row.addWidget(c)
144
+ self._render()
145
+
146
+ def _step(self, d) -> None:
147
+ stages = JOURNEYS[self._mode]
148
+ self._i = max(0, min(len(stages) - 1, self._i + d))
149
+ self._render()
150
+
151
+ def _live_note(self, title) -> str:
152
+ """The real-values line appended to a trap-entry stage caption. Prefers a frozen trap
153
+ (TrapFrame) — its actual saved registers and scause — else falls back to the running
154
+ proc's live registers at the dispatch stage."""
155
+ fr = self.frame
156
+ if fr is not None and getattr(fr, "ok", False):
157
+ r = fr.regs
158
+ if title == "uservec":
159
+ parts = [f"{k}={r[k]}" for k in ("ra", "sp", "a0", "a7") if k in r]
160
+ return ("this trap's user registers, saved into the trapframe: " + " ".join(parts)
161
+ if parts else "this trap's user registers are saved into the trapframe")
162
+ if title == "usertrap":
163
+ s = f"scause={fr.scause} → {fr.kind_name} · sepc={fr.sepc}"
164
+ if fr.stval and fr.stval not in ("0x0", "0x0000000000000000"):
165
+ s += f" · stval (faulting address) = {fr.stval}"
166
+ return s
167
+ if title == "syscall()":
168
+ a7, a0 = r.get("a7", ""), r.get("a0", "")
169
+ return f"live: a7={a7} a0={a0}" if (a7 or a0) else ""
170
+ return ""
171
+ # no frozen trap — the original behaviour: seed the dispatch stage from the running proc
172
+ if title == "syscall()" and self.cpu is not None:
173
+ return (f"live: a7={self.cpu.key('a7')} a0={self.cpu.key('a0')} "
174
+ f"pc={self.cpu.key('pc')}")
175
+ return ""
176
+
177
+ def _render(self) -> None:
178
+ t = self.theme.theme
179
+ stages = JOURNEYS[self._mode]
180
+ s = stages[self._i]
181
+ for idx, c in enumerate(self._chips):
182
+ active = idx == self._i
183
+ done = idx < self._i
184
+ col = t.accent_for("blue") if active else (t.text if done else t.muted)
185
+ border = t.accent if active else t.line
186
+ c.setStyleSheet(
187
+ f"color:{col};background:{t.panel2};border:1px solid {border};"
188
+ f"border-radius:6px;padding:4px 6px;font-size:11px;"
189
+ f"{'font-weight:600;' if active else ''}")
190
+ lane = {"A": "process A", "sched": "scheduler", "B": "process B"}.get(s.lane, s.lane)
191
+ self._band.setText(f"CPU is in: {s.band.upper()} mode · {lane}"
192
+ + (f" · privilege {'change' if s.band == 'user' else 'stays S'}"
193
+ if self._mode != 'context' else " · never leaves S"))
194
+ # seed the trap-entry captions with real values — from a frozen trap if we have one,
195
+ # else the running proc's registers (the old behaviour), else nothing.
196
+ note = self._live_note(s.title) if self._mode == "syscall" else ""
197
+ self._caption.setText(s.caption + (f"\n\n{note}" if note else ""))
198
+ # highlight the active save-area
199
+ for card, kind in ((self._tf, "trapframe"), (self._ctx, "context")):
200
+ on = s.save == kind
201
+ accent = t.accent_for("green" if kind == "trapframe" else "amber")
202
+ card.setStyleSheet(
203
+ f"QFrame#{kind}{{background:{t.panel2};border:1px solid "
204
+ f"{accent if on else t.line};border-radius:10px;}}")
205
+ card._title.setStyleSheet(
206
+ f"color:{accent if on else t.text};font-size:13px;font-weight:600;border:none;"
207
+ + (f"" if on else ""))
208
+ card._sub.setStyleSheet(f"color:{t.muted};font-size:11px;border:none;")
209
+ card._title.setText(("▸ " if on else "") + kind + (" (writing)" if on else ""))
210
+ self._pos.setText(f"stage {self._i + 1} / {len(stages)}")
211
+ self._prev.setEnabled(self._i > 0)
212
+ self._next.setEnabled(self._i < len(stages) - 1)
gini/ui/cpu_lab.py ADDED
@@ -0,0 +1,306 @@
1
+ """CPU & Registers face of the xv6 Machine — the HARDWARE / privilege view.
2
+
3
+ Deliberately NOT a subset of the Process Scheduler (which is about *which* process runs). This is the
4
+ CPU's privilege-and-trap machinery, all read from the live kernel with no halt:
5
+
6
+ • Mode-time bar — user / kernel / idle split of the last second, sampled at each timer tick by the
7
+ trap entry path (usertrap=user, kerneltrap=kernel|idle). The honest 'how busy, and where'.
8
+ • CSR / interrupt strip — the three S-mode interrupt sources (enabled + pending) from sie/sip, the
9
+ came-from privilege (sstatus.SPP), the trap vector (stvec), and the last trap cause (scause).
10
+ • Register tiles — the saved user register file per hart, as colored tiles grouped by role.
11
+
12
+ The momentary global interrupt bit (sstatus.SIE) reads 0 during a dump (we're inside a handler), so
13
+ the strip leans on the enable CONFIG (sie) for the honest interrupt state — see interrupt_sources().
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from PySide6.QtCore import Qt, QTimer, Signal
18
+ from PySide6.QtGui import QColor, QPainter
19
+ from PySide6.QtWidgets import (
20
+ QFrame, QGridLayout, QHBoxLayout, QLabel, QVBoxLayout, QWidget,
21
+ )
22
+
23
+ from ..domain.xv6 import (
24
+ interrupt_sources, mode_split, scause_str, sstatus_flags,
25
+ )
26
+ from .theme import ThemeManager, icons
27
+ from .theme.manager import scale_css as _scss
28
+
29
+ # register tiles grouped by role -> (row of reg names, accent key)
30
+ _REG_GROUPS = [
31
+ ("instruction", ["pc"], "blue"),
32
+ ("stack", ["sp", "s0"], "amber"),
33
+ ("return", ["ra"], "purple"),
34
+ ("arguments", ["a0", "a7"], "green"),
35
+ ("paging", ["satp"], "red"),
36
+ ("mem size", ["sz"], "slate"),
37
+ ]
38
+
39
+
40
+ def _decode_satp(satp: str) -> str:
41
+ """Sv39 satp -> mode + physical page-table root (PPN << 12)."""
42
+ try:
43
+ v = int(satp, 16)
44
+ except (TypeError, ValueError):
45
+ return ""
46
+ mode = (v >> 60) & 0xF
47
+ ppn = v & ((1 << 44) - 1)
48
+ return {0: "Bare", 8: "Sv39", 9: "Sv48"}.get(mode, f"mode {mode}") + f" · root {hex(ppn << 12)}"
49
+
50
+
51
+ class SegmentBar(QWidget):
52
+ """A horizontal proportional bar: [(label, fraction, color)] painted end to end."""
53
+
54
+ def __init__(self, parent=None) -> None:
55
+ super().__init__(parent)
56
+ self._segs: list = []
57
+ self.setMinimumHeight(30)
58
+
59
+ def set_segments(self, segs) -> None:
60
+ self._segs = [s for s in segs if s[1] > 0]
61
+ self.update()
62
+
63
+ def paintEvent(self, _e) -> None: # noqa: N802
64
+ p = QPainter(self)
65
+ p.setRenderHint(QPainter.Antialiasing)
66
+ w, h = self.width(), self.height()
67
+ x = 0.0
68
+ for label, frac, color in self._segs:
69
+ seg_w = w * frac
70
+ p.setBrush(QColor(color))
71
+ p.setPen(Qt.NoPen)
72
+ p.drawRoundedRect(int(x) + 1, 0, int(seg_w) - 2, h, 5, 5)
73
+ if seg_w > 46: # only label a wide-enough segment
74
+ p.setPen(QColor("#ffffff"))
75
+ p.drawText(int(x), 0, int(seg_w), h, Qt.AlignCenter,
76
+ f"{label} {round(frac * 100)}%")
77
+ x += seg_w
78
+
79
+
80
+ class CpuLab(QWidget):
81
+ snap_ready = Signal(object)
82
+
83
+ def __init__(self, parent, theme: ThemeManager, device, state, live=False) -> None:
84
+ super().__init__(parent)
85
+ self.setWindowFlag(Qt.Window, True)
86
+ self.theme = theme
87
+ self.device = device
88
+ self.state = state
89
+ self.live = live
90
+ self._closed = False
91
+ self._busy = False
92
+ self._prev_modetime: dict | None = None
93
+
94
+ t = theme.theme
95
+ self.setWindowTitle(f"CPU & Registers Lab — {device.name}")
96
+ self.resize(860, 620)
97
+ self.setStyleSheet(f"QWidget{{background:{t.bg};}}")
98
+ root = QVBoxLayout(self)
99
+ root.setSpacing(12)
100
+
101
+ head = QHBoxLayout()
102
+ ic = QLabel(); ic.setPixmap(icons.render_pixmap("host", t.accent_for("red"), 22))
103
+ title = QLabel(f" CPU & Registers Lab — {device.name}")
104
+ title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
105
+ head.addWidget(ic); head.addWidget(title); head.addStretch(1)
106
+ mode = QLabel("live" if live else "offline demo")
107
+ mode.setStyleSheet(
108
+ f"color:{t.success if live else t.muted};background:{t.panel2};"
109
+ f"border:1px solid {t.line};border-radius:9px;padding:2px 10px;font-size:11px;")
110
+ head.addWidget(mode)
111
+ root.addLayout(head)
112
+
113
+ # 1) mode-time bar
114
+ self._bar = SegmentBar()
115
+ self._bar_note = QLabel()
116
+ self._bar_note.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
117
+ root.addWidget(self._panel("Where the CPU spends time · user / kernel / idle (last ~1s)",
118
+ self._bar, self._bar_note))
119
+
120
+ # 2) CSR / interrupt strip
121
+ self._csr_box = QWidget()
122
+ self._csr_lay = QHBoxLayout(self._csr_box)
123
+ self._csr_lay.setContentsMargins(0, 0, 0, 0)
124
+ self._csr_lay.setSpacing(6)
125
+ self._csr_note = QLabel()
126
+ self._csr_note.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
127
+ root.addWidget(self._panel("Traps & interrupts · control CSRs (this hart)",
128
+ self._csr_box, self._csr_note))
129
+
130
+ # 3) register tiles (per core)
131
+ self._tiles_box = QWidget()
132
+ self._tiles_lay = QVBoxLayout(self._tiles_box)
133
+ self._tiles_lay.setContentsMargins(0, 0, 0, 0)
134
+ self._tiles_lay.setSpacing(10)
135
+ root.addWidget(self._panel("Register file · saved user context, per core",
136
+ self._tiles_box), 1)
137
+ root.addStretch(0)
138
+
139
+ self.snap_ready.connect(self._on_snap)
140
+ self._poll = QTimer(self)
141
+ self._poll.timeout.connect(lambda: self._fetch())
142
+ self._render()
143
+ if self.live:
144
+ self._poll.start(1000)
145
+ self._fetch()
146
+
147
+ # -- panels / chips / tiles -------------------------------------------- #
148
+ def _panel(self, title, *inners) -> QFrame:
149
+ t = self.theme.theme
150
+ f = QFrame(); f.setStyleSheet(
151
+ _scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
152
+ v = QVBoxLayout(f); v.setContentsMargins(12, 9, 12, 11); v.setSpacing(7)
153
+ h = QLabel(title)
154
+ h.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
155
+ v.addWidget(h)
156
+ for inner in inners:
157
+ inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
158
+ v.addWidget(inner)
159
+ return f
160
+
161
+ def _chip(self, text, color, filled=False, dot=False) -> QLabel:
162
+ t = self.theme.theme
163
+ lbl = QLabel(("● " if dot else "") + text)
164
+ if filled:
165
+ lbl.setStyleSheet(_scss(
166
+ f"color:#ffffff;background:{color};border-radius:8px;padding:3px 9px;"
167
+ "font-size:11px;font-weight:600;"))
168
+ else:
169
+ lbl.setStyleSheet(_scss(
170
+ f"color:{t.muted};background:{t.panel};border:1px solid {t.line};"
171
+ "border-radius:8px;padding:3px 9px;font-size:11px;"))
172
+ return lbl
173
+
174
+ def _reg_tile(self, name, value, color, subtitle="") -> QFrame:
175
+ t = self.theme.theme
176
+ f = QFrame()
177
+ f.setStyleSheet(_scss(
178
+ f"QFrame{{background:{t.panel};border:1px solid {t.line};"
179
+ f"border-left:3px solid {color};border-radius:7px;}}"))
180
+ v = QVBoxLayout(f); v.setContentsMargins(9, 5, 9, 6); v.setSpacing(1)
181
+ n = QLabel(name.upper())
182
+ n.setStyleSheet(_scss(f"color:{color};font-size:10px;font-weight:700;border:none;"))
183
+ val = QLabel(value)
184
+ val.setStyleSheet(_scss(f"color:{t.text};font-family:monospace;font-size:13px;border:none;"))
185
+ v.addWidget(n); v.addWidget(val)
186
+ if subtitle:
187
+ s = QLabel(subtitle)
188
+ s.setStyleSheet(_scss(f"color:{t.faint};font-size:10px;border:none;"))
189
+ v.addWidget(s)
190
+ return f
191
+
192
+ @staticmethod
193
+ def _clear(layout) -> None:
194
+ while layout.count():
195
+ it = layout.takeAt(0)
196
+ w = it.widget()
197
+ if w is not None:
198
+ w.setParent(None)
199
+
200
+ # -- data -------------------------------------------------------------- #
201
+ def _fetch(self) -> None:
202
+ if self._busy or self._closed:
203
+ return
204
+ self._busy = True
205
+ import threading
206
+
207
+ def work():
208
+ try:
209
+ self.state.refresh()
210
+ if not self._closed:
211
+ self.snap_ready.emit(None)
212
+ except (Exception, RuntimeError):
213
+ pass
214
+ threading.Thread(target=work, daemon=True).start()
215
+
216
+ def _on_snap(self, _obj) -> None:
217
+ self._busy = False
218
+ if not self._closed:
219
+ self._render()
220
+
221
+ def _render(self) -> None:
222
+ snap = self.state.latest
223
+ if snap is None:
224
+ return
225
+ t = self.theme.theme
226
+ self._render_modebar(snap, t)
227
+ self._render_csr(snap, t)
228
+ self._render_tiles(snap, t)
229
+
230
+ def _render_modebar(self, snap, t) -> None:
231
+ if not snap.modetime:
232
+ self._bar.set_segments([])
233
+ self._bar_note.setText("mode-time needs the kernel rebuild (MODETIME line).")
234
+ return
235
+ split = mode_split(self._prev_modetime, snap.modetime)
236
+ self._prev_modetime = dict(snap.modetime)
237
+ self._bar.set_segments([
238
+ ("user", split["user"], t.accent_for("green")),
239
+ ("kernel", split["kernel"], t.accent_for("amber")),
240
+ ("idle", split["idle"], t.accent_for("slate")),
241
+ ])
242
+ if sum(split.values()) == 0:
243
+ self._bar_note.setText("sampling… (needs a second timer tick to show a delta)")
244
+ else:
245
+ self._bar_note.setText(
246
+ f"user {round(split['user']*100)}% · kernel {round(split['kernel']*100)}% "
247
+ f"· idle {round(split['idle']*100)}% — sampled at each timer interrupt")
248
+
249
+ def _render_csr(self, snap, t) -> None:
250
+ self._clear(self._csr_lay)
251
+ csr = snap.csr
252
+ if not csr:
253
+ self._csr_note.setText("control CSRs need the kernel rebuild (CSR line).")
254
+ return
255
+ # interrupt sources: enabled -> filled+colored, pending -> a dot
256
+ for src in interrupt_sources(csr.get("sie", 0), csr.get("sip", 0)):
257
+ self._csr_lay.addWidget(self._chip(
258
+ src["name"], t.accent_for("purple"),
259
+ filled=src["enabled"], dot=src["pending"]))
260
+ self._csr_lay.addSpacing(8)
261
+ flags = sstatus_flags(csr.get("sstatus", 0))
262
+ came = "user" if flags["SPP"] == "U" else "kernel"
263
+ self._csr_lay.addWidget(self._chip(
264
+ f"came from {came}", t.accent_for("blue"), filled=True))
265
+ self._csr_lay.addStretch(1)
266
+ self._csr_note.setText(
267
+ f"trap vector (stvec) {hex(csr.get('stvec', 0))} · "
268
+ f"last trap (scause): {scause_str(csr.get('scause'))} · "
269
+ "interrupt state = enabled sources above (global SIE reads 0 mid-dump)")
270
+
271
+ def _render_tiles(self, snap, t) -> None:
272
+ self._clear(self._tiles_lay)
273
+ cpu_regs = snap.cpu_regs or ({0: snap.cpu} if snap.cpu else {})
274
+ if not cpu_regs:
275
+ self._tiles_lay.addWidget(QLabel("no running process on any core"))
276
+ return
277
+ for c in sorted(cpu_regs):
278
+ cs = cpu_regs[c]
279
+ pid = cs.key("pid")
280
+ sec = QFrame()
281
+ sv = QVBoxLayout(sec); sv.setContentsMargins(0, 0, 0, 0); sv.setSpacing(6)
282
+ hdr = QLabel(f"CPU {c}" + (f" · pid {pid}" if pid not in ("—",) else " · idle"))
283
+ hdr.setStyleSheet(_scss(f"color:{t.text};font-size:12px;font-weight:600;border:none;"))
284
+ sv.addWidget(hdr)
285
+ grid = QGridLayout(); grid.setSpacing(7)
286
+ col = 0
287
+ for _group, names, key in _REG_GROUPS:
288
+ color = t.accent_for(key)
289
+ for name in names:
290
+ val = cs.key(name)
291
+ sub = _decode_satp(val) if name == "satp" else ""
292
+ grid.addWidget(self._reg_tile(name, val, color, sub), 0, col)
293
+ col += 1
294
+ grid.setColumnStretch(col, 1)
295
+ gw = QWidget(); gw.setLayout(grid)
296
+ sv.addWidget(gw)
297
+ self._tiles_lay.addWidget(sec)
298
+
299
+ def closeEvent(self, e) -> None: # noqa: N802
300
+ self._closed = True
301
+ self._poll.stop()
302
+ try:
303
+ self.snap_ready.disconnect(self._on_snap)
304
+ except (RuntimeError, TypeError):
305
+ pass
306
+ super().closeEvent(e)