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,184 @@
1
+ """Registry of Diagnose games, so the Games hub card and each Lab can list/open them uniformly.
2
+
3
+ An entry knows how to BUILD its DiagnoseGameWidget given (theme, machine_state, live). Games that read
4
+ current state cheaply (policy/fault/trap) build live cases per draw; the process game needs
5
+ accumulation over time, so its catalog entry uses the demo deck (the live version lives in the
6
+ Fingerprint Lab, which owns the accumulator)."""
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from PySide6.QtWidgets import QWidget
12
+
13
+
14
+ @dataclass
15
+ class GameEntry:
16
+ id: str
17
+ title: str
18
+ subtitle: str
19
+ build: object # (theme, state, live) -> QWidget
20
+
21
+
22
+ _ENTRIES: dict = {}
23
+
24
+
25
+ def register(entry: GameEntry) -> None:
26
+ _ENTRIES[entry.id] = entry
27
+
28
+
29
+ def catalog() -> list:
30
+ return list(_ENTRIES.values())
31
+
32
+
33
+ def get(game_id: str):
34
+ return _ENTRIES.get(game_id)
35
+
36
+
37
+ def _build_process(theme, state, live) -> QWidget:
38
+ from .diagnose_game import DiagnoseGameWidget
39
+ from .game_renderers import RadarChart
40
+ from ..domain.games.process_game import PROCESS_SPEC, demo_cases
41
+ return DiagnoseGameWidget(theme, PROCESS_SPEC, demo_cases, RadarChart(theme), live=False)
42
+
43
+
44
+ register(GameEntry("process-classify", "Classify the process",
45
+ "Name a process from its behavioral signature.", _build_process))
46
+
47
+
48
+ def _build_policy(theme, state, live) -> QWidget:
49
+ from .diagnose_game import DiagnoseGameWidget
50
+ from .game_renderers import GanttSnippet
51
+ from ..domain.games.policy_game import POLICY_SPEC, demo_cases, live_cases
52
+ from ..domain.xv6 import POLICY_NAMES
53
+
54
+ def source():
55
+ if live and state is not None and state.latest is not None:
56
+ pids = [getattr(s, "pid", None) for s in state.timeline.recent()]
57
+ pol = getattr(state.provider, "kernel_policy", None)
58
+ name = POLICY_NAMES.get(pol) if pol is not None else getattr(state, "policy", None)
59
+ cases = live_cases(pids, name)
60
+ if cases:
61
+ return cases
62
+ return demo_cases()
63
+
64
+ return DiagnoseGameWidget(theme, POLICY_SPEC, source, GanttSnippet(theme), live=live)
65
+
66
+
67
+ register(GameEntry("guess-policy", "Guess the scheduler",
68
+ "Read a Gantt timeline and name the scheduling policy.", _build_policy))
69
+
70
+
71
+ def _read_traps(state) -> str:
72
+ fn = getattr(getattr(state, "provider", None), "traps", None)
73
+ try:
74
+ return fn() if callable(fn) else ""
75
+ except Exception:
76
+ return ""
77
+
78
+
79
+ def _build_trap(theme, state, live) -> QWidget:
80
+ from .diagnose_game import DiagnoseGameWidget
81
+ from .game_renderers import EventCard
82
+ from ..domain.games.trap_game import TRAP_SPEC, trap_demo_cases, trap_live_cases
83
+ from ..domain.xv6 import parse_traptrace
84
+
85
+ def source():
86
+ if live and state is not None:
87
+ cases = trap_live_cases(parse_traptrace(_read_traps(state)))
88
+ if cases:
89
+ return cases
90
+ return trap_demo_cases()
91
+
92
+ return DiagnoseGameWidget(theme, TRAP_SPEC, source, EventCard(theme), live=live)
93
+
94
+
95
+ def _build_fault(theme, state, live) -> QWidget:
96
+ from .diagnose_game import DiagnoseGameWidget
97
+ from .game_renderers import EventCard
98
+ from ..domain.games.trap_game import FAULT_SPEC, fault_demo_cases, fault_live_cases
99
+ from ..domain.xv6 import parse_traptrace
100
+
101
+ def source():
102
+ if live and state is not None:
103
+ cases = fault_live_cases(parse_traptrace(_read_traps(state)))
104
+ if cases:
105
+ return cases
106
+ return fault_demo_cases()
107
+
108
+ return DiagnoseGameWidget(theme, FAULT_SPEC, source, EventCard(theme), live=live)
109
+
110
+
111
+ register(GameEntry("trap-cause", "Decode the trap",
112
+ "Read a raw scause and name the trap kind.", _build_trap))
113
+ register(GameEntry("fault-type", "Decode the page fault",
114
+ "Read a page-fault scause and name the access.", _build_fault))
115
+
116
+
117
+ def _build_thrash(theme, state, live) -> QWidget:
118
+ # simulator-backed (real FIFO/LRU/OPT over reference strings): offline deck. A live xv6
119
+ # page-replacement mechanism would add real cases later.
120
+ from .diagnose_game import DiagnoseGameWidget
121
+ from .game_renderers import PagingCard
122
+ from ..domain.games.thrash_game import THRASH_SPEC, demo_cases
123
+ return DiagnoseGameWidget(theme, THRASH_SPEC, demo_cases, PagingCard(theme), live=False)
124
+
125
+
126
+ register(GameEntry("thrash-diagnose", "Diagnose the thrashing",
127
+ "Read a paging run's signature and name the cause.", _build_thrash))
128
+
129
+
130
+ def _paging_game(spec, cases):
131
+ def build(theme, state, live) -> QWidget:
132
+ from .diagnose_game import DiagnoseGameWidget
133
+ from .game_renderers import RefStringCard
134
+ return DiagnoseGameWidget(theme, spec, cases, RefStringCard(theme), live=False)
135
+ return build
136
+
137
+
138
+ def _register_paging_cluster() -> None:
139
+ from ..domain.games.paging_games import (
140
+ BELADY_SPEC, FAULTCOUNT_SPEC, NEXTEVICT_SPEC, POLICYRANK_SPEC, SHOWDOWN_SPEC,
141
+ belady_cases, faultcount_cases, nextevict_cases, policyrank_cases, showdown_cases,
142
+ )
143
+ register(GameEntry("faults-estimate", "Count the page faults",
144
+ "Estimate how many faults a run takes.",
145
+ _paging_game(FAULTCOUNT_SPEC, faultcount_cases)))
146
+ register(GameEntry("belady-spot", "Belady spotter",
147
+ "Predict whether one more frame helps FIFO.",
148
+ _paging_game(BELADY_SPEC, belady_cases)))
149
+ register(GameEntry("policy-showdown", "Policy showdown",
150
+ "FIFO or LRU — which faults fewer on this string?",
151
+ _paging_game(SHOWDOWN_SPEC, showdown_cases)))
152
+ register(GameEntry("next-evict", "Spot the next eviction",
153
+ "Pick which resident page is evicted next.",
154
+ _paging_game(NEXTEVICT_SPEC, nextevict_cases)))
155
+ register(GameEntry("policy-rank", "Rank the policies",
156
+ "Order FIFO / LRU / OPT by faults.",
157
+ _paging_game(POLICYRANK_SPEC, policyrank_cases)))
158
+
159
+
160
+ _register_paging_cluster()
161
+
162
+
163
+ def _build_translate(theme, state, live) -> QWidget:
164
+ from .diagnose_game import DiagnoseGameWidget
165
+ from .game_renderers import TranslateCard
166
+ from ..domain.games.translate_game import TRANSLATE_SPEC, demo_cases, live_cases
167
+
168
+ def source():
169
+ if live and state is not None and getattr(state, "vm", None) is not None:
170
+ try:
171
+ snap = state.vm.snapshot()
172
+ cases = live_cases(getattr(snap, "leaves", []))
173
+ if cases:
174
+ return cases
175
+ except Exception:
176
+ pass
177
+ return demo_cases()
178
+
179
+ return DiagnoseGameWidget(theme, TRANSLATE_SPEC, source, TranslateCard(theme), live=live)
180
+
181
+
182
+ register(GameEntry("addr-translate", "Translate the address",
183
+ "Compute a physical address from a page table (plays live page tables).",
184
+ _build_translate))
@@ -0,0 +1,340 @@
1
+ """Signature renderers for the Diagnose games (and the Fingerprint Explore tab).
2
+
3
+ Each renderer is a small QWidget painter with a `show_signature(sig)` method the generic
4
+ DiagnoseGameWidget calls to draw a mystery case. RadarChart/ScatterBoard also back the Fingerprint
5
+ Explore tab. GanttSnippet (policy game) and EventCard (fault/trap games) live here too.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+
11
+ from PySide6.QtCore import QPointF, Qt
12
+ from PySide6.QtGui import QColor, QPainter, QPolygonF
13
+ from PySide6.QtWidgets import QWidget
14
+
15
+ from ..domain.fingerprint import AXIS_LABEL, FEATURE_AXES
16
+
17
+
18
+ class RadarChart(QWidget):
19
+ """A pentagon radar over FEATURE_AXES; one or more translucent polygons."""
20
+
21
+ def __init__(self, theme, parent=None) -> None:
22
+ super().__init__(parent)
23
+ self.theme = theme
24
+ self._series: list = [] # [(label, fp, color_key)]
25
+ self.setMinimumSize(320, 280)
26
+
27
+ def set_series(self, series) -> None:
28
+ self._series = series
29
+ self.update()
30
+
31
+ def show_signature(self, fp) -> None:
32
+ """DiagnoseGameWidget renderer hook: draw a single mystery fingerprint."""
33
+ self.set_series([("?", fp, "purple")] if fp else [])
34
+
35
+ def paintEvent(self, _e) -> None: # noqa: N802
36
+ t = self.theme.theme
37
+ p = QPainter(self)
38
+ p.setRenderHint(QPainter.Antialiasing)
39
+ cx, cy = self.width() / 2, self.height() / 2 + 4
40
+ r = min(cx, cy) - 52 # leave room so the edge labels never clip
41
+ n = len(FEATURE_AXES)
42
+ p.setPen(QColor(t.line))
43
+ for frac in (0.25, 0.5, 0.75, 1.0):
44
+ poly = QPolygonF([self._pt(cx, cy, r * frac, i, n) for i in range(n)])
45
+ p.drawPolygon(poly)
46
+ p.setPen(QColor(t.muted))
47
+ bw = 60
48
+ for i, ax in enumerate(FEATURE_AXES):
49
+ pt = self._pt(cx, cy, r + 16, i, n)
50
+ if pt.x() > cx + 5:
51
+ x, flag = pt.x(), Qt.AlignLeft
52
+ elif pt.x() < cx - 5:
53
+ x, flag = pt.x() - bw, Qt.AlignRight
54
+ else:
55
+ x, flag = pt.x() - bw / 2, Qt.AlignHCenter
56
+ p.drawText(int(x), int(pt.y() - 8), bw, 16, flag | Qt.AlignVCenter, AXIS_LABEL[ax])
57
+ for _label, fp, key in self._series:
58
+ col = QColor(t.accent_for(key))
59
+ poly = QPolygonF([self._pt(cx, cy, r * fp[ax], i, n)
60
+ for i, ax in enumerate(FEATURE_AXES)])
61
+ fill = QColor(col); fill.setAlpha(60)
62
+ p.setBrush(fill); p.setPen(col)
63
+ p.drawPolygon(poly)
64
+
65
+ @staticmethod
66
+ def _pt(cx, cy, rad, i, n) -> QPointF:
67
+ ang = -math.pi / 2 + i * 2 * math.pi / n
68
+ return QPointF(cx + rad * math.cos(ang), cy + rad * math.sin(ang))
69
+
70
+
71
+ class ScatterBoard(QWidget):
72
+ """The behavior map: x = CPU-bound <-> IO-bound, y = compute <-> heavy-kernel."""
73
+
74
+ def __init__(self, theme, parent=None) -> None:
75
+ super().__init__(parent)
76
+ self.theme = theme
77
+ self._points: list = [] # [(x, y, label, color_key)]
78
+ self.setMinimumSize(300, 240)
79
+
80
+ def set_points(self, points) -> None:
81
+ self._points = points
82
+ self.update()
83
+
84
+ def paintEvent(self, _e) -> None: # noqa: N802
85
+ t = self.theme.theme
86
+ p = QPainter(self)
87
+ p.setRenderHint(QPainter.Antialiasing)
88
+ m = 30
89
+ w, h = self.width() - 2 * m, self.height() - 2 * m
90
+ p.setPen(QColor(t.line))
91
+ p.drawRect(m, m, w, h)
92
+ p.drawLine(int(m + w / 2), m, int(m + w / 2), m + h)
93
+ p.drawLine(m, int(m + h / 2), m + w, int(m + h / 2))
94
+ p.setPen(QColor(t.muted))
95
+ p.drawText(m, m + h + 4, w, 16, Qt.AlignLeft, "IO-bound")
96
+ p.drawText(m, m + h + 4, w, 16, Qt.AlignRight, "CPU-bound")
97
+ p.save(); p.translate(m - 8, m + h); p.rotate(-90)
98
+ p.drawText(0, 0, h, 16, Qt.AlignLeft, "compute")
99
+ p.drawText(0, 0, h, 16, Qt.AlignRight, "kernel")
100
+ p.restore()
101
+ for x, y, label, key in self._points:
102
+ px = m + x * w
103
+ py = m + (1 - y) * h
104
+ col = QColor(t.accent_for(key))
105
+ p.setBrush(col); p.setPen(Qt.NoPen)
106
+ p.drawEllipse(QPointF(px, py), 6, 6)
107
+ p.setPen(QColor(t.text))
108
+ p.drawText(int(px + 8), int(py + 4), label)
109
+
110
+
111
+ class GanttSnippet(QWidget):
112
+ """A short running-pid timeline (the guess-the-policy signature). set via a list of pids."""
113
+
114
+ def __init__(self, theme, parent=None) -> None:
115
+ super().__init__(parent)
116
+ self.theme = theme
117
+ self._slots: list = [] # [pid or None] over time
118
+ self.setMinimumSize(320, 130)
119
+
120
+ def show_signature(self, slots) -> None:
121
+ self._slots = list(slots or [])
122
+ self.update()
123
+
124
+ def _color(self, pid) -> str:
125
+ keys = ["blue", "green", "purple", "amber", "teal", "pink", "orange"]
126
+ return keys[pid % len(keys)] if pid is not None else "slate"
127
+
128
+ def paintEvent(self, _e) -> None: # noqa: N802
129
+ t = self.theme.theme
130
+ p = QPainter(self)
131
+ p.setRenderHint(QPainter.Antialiasing)
132
+ if not self._slots:
133
+ p.setPen(QColor(t.faint))
134
+ p.drawText(self.rect(), Qt.AlignCenter, "— no timeline —")
135
+ return
136
+ m = 12
137
+ w = (self.width() - 2 * m) / max(len(self._slots), 1)
138
+ h = self.height() - 2 * m
139
+ from ..domain.xv6 import short_pid
140
+ for i, pid in enumerate(self._slots):
141
+ x = m + i * w
142
+ col = QColor(t.accent_for(self._color(pid))) if pid is not None else QColor(t.panel2)
143
+ p.fillRect(int(x), m, int(w) + 1, h, col)
144
+ if pid is not None and (i == 0 or self._slots[i - 1] != pid) and w >= 14:
145
+ p.setPen(QColor("#111111"))
146
+ p.drawText(int(x), m, int(w), h, Qt.AlignCenter, short_pid(pid))
147
+
148
+
149
+ class RefStringCard(QWidget):
150
+ """A page-reference string + frames/policy — the signature for the paging cluster games.
151
+ signature = {"refs": [...], "frames": n, "policy": "LRU"?, "note": str?}."""
152
+
153
+ def __init__(self, theme, parent=None) -> None:
154
+ super().__init__(parent)
155
+ self.theme = theme
156
+ self._s: dict = {}
157
+ self.setMinimumSize(320, 160)
158
+
159
+ def show_signature(self, s) -> None:
160
+ self._s = s or {}
161
+ self.update()
162
+
163
+ def _color(self, pg) -> str:
164
+ keys = ["blue", "green", "purple", "amber", "teal", "pink", "orange", "indigo"]
165
+ return keys[pg % len(keys)]
166
+
167
+ def paintEvent(self, _e) -> None: # noqa: N802
168
+ t = self.theme.theme
169
+ p = QPainter(self)
170
+ p.setRenderHint(QPainter.Antialiasing)
171
+ s = self._s
172
+ if not s:
173
+ p.setPen(QColor(t.faint)); p.drawText(self.rect(), Qt.AlignCenter, "— no run —")
174
+ return
175
+ m = 14
176
+ head = f"frames: {s.get('frames', '?')}"
177
+ if s.get("policy"):
178
+ head += f" policy: {s['policy']}"
179
+ p.setPen(QColor(t.text))
180
+ p.drawText(m, m, self.width() - 2 * m, 18, Qt.AlignLeft, head)
181
+ # the reference string as a wrapped row of numbered cells
182
+ refs = s.get("refs", [])
183
+ cw, ch, gap = 26, 24, 6
184
+ x, y = m, m + 30
185
+ p.setFont(self.font())
186
+ for pg in refs:
187
+ if x + cw > self.width() - m:
188
+ x = m; y += ch + gap
189
+ col = QColor(t.accent_for(self._color(pg)))
190
+ p.setBrush(QColor(t.panel)); p.setPen(col)
191
+ p.drawRoundedRect(x, y, cw, ch, 5, 5)
192
+ p.setPen(QColor(t.text))
193
+ p.drawText(x, y, cw, ch, Qt.AlignCenter, str(pg))
194
+ x += cw + gap
195
+ if s.get("note"):
196
+ p.setPen(QColor(t.muted))
197
+ p.drawText(m, self.height() - 22, self.width() - 2 * m, 16, Qt.AlignLeft, s["note"])
198
+
199
+
200
+ class TranslateCard(QWidget):
201
+ """A page table + a target VA — the address-translation signature.
202
+ signature = {"rows": [(va_base, pa_base, perms)], "va": int}."""
203
+
204
+ def __init__(self, theme, parent=None) -> None:
205
+ super().__init__(parent)
206
+ self.theme = theme
207
+ self._s: dict = {}
208
+ self.setMinimumSize(340, 200)
209
+
210
+ def show_signature(self, s) -> None:
211
+ self._s = s or {}
212
+ self.update()
213
+
214
+ def paintEvent(self, _e) -> None: # noqa: N802
215
+ t = self.theme.theme
216
+ p = QPainter(self)
217
+ p.setRenderHint(QPainter.Antialiasing)
218
+ s = self._s
219
+ if not s:
220
+ p.setPen(QColor(t.faint)); p.drawText(self.rect(), Qt.AlignCenter, "— no mapping —")
221
+ return
222
+ m = 14
223
+ va = s.get("va", 0)
224
+ rows = s.get("rows", [])
225
+ p.setPen(QColor(t.muted))
226
+ p.drawText(m, m, self.width() - 2 * m, 16, Qt.AlignLeft, "page table · leaf mappings")
227
+ y = m + 22
228
+ for vb, pb, perm in rows:
229
+ hit = vb <= va < vb + 4096
230
+ p.setPen(QColor(t.accent if hit else t.line))
231
+ p.setBrush(QColor(t.panel2) if hit else QColor(t.panel))
232
+ p.drawRoundedRect(m, y, self.width() - 2 * m, 22, 5, 5)
233
+ p.setPen(QColor(t.text))
234
+ p.drawText(m + 8, y, self.width() - 2 * m, 22, Qt.AlignVCenter,
235
+ f"VA {hex(vb)} → PA {hex(pb)} {perm}")
236
+ y += 26
237
+ p.setPen(QColor(t.accent_for("purple")))
238
+ f = self.font(); f.setBold(True); p.setFont(f)
239
+ p.drawText(m, y + 6, self.width() - 2 * m, 22, Qt.AlignLeft,
240
+ f"translate VA = {hex(va)} → PA = ?")
241
+
242
+
243
+ class PagingCard(QWidget):
244
+ """The thrashing signature: labeled meters (fault rate, locality, working-set growth) + the
245
+ frames-vs-working-set comparison. signature = the run_features() dict."""
246
+
247
+ def __init__(self, theme, parent=None) -> None:
248
+ super().__init__(parent)
249
+ self.theme = theme
250
+ self._f: dict = {}
251
+ self.setMinimumSize(320, 200)
252
+
253
+ def show_signature(self, f) -> None:
254
+ self._f = f or {}
255
+ self.update()
256
+
257
+ def _meter(self, p, x, y, w, label, val, key) -> None:
258
+ t = self.theme.theme
259
+ p.setPen(QColor(t.muted))
260
+ p.drawText(int(x), int(y), 120, 16, Qt.AlignLeft | Qt.AlignVCenter, label)
261
+ bx = x + 124
262
+ bw = w - 124
263
+ p.setBrush(QColor(t.panel)); p.setPen(QColor(t.line))
264
+ p.drawRoundedRect(int(bx), int(y + 2), int(bw), 12, 4, 4)
265
+ p.setBrush(QColor(t.accent_for(key))); p.setPen(Qt.NoPen)
266
+ p.drawRoundedRect(int(bx), int(y + 2), int(bw * max(0.0, min(1.0, val))), 12, 4, 4)
267
+ p.setPen(QColor(t.text))
268
+ p.drawText(int(bx + bw - 44), int(y), 44, 16, Qt.AlignRight | Qt.AlignVCenter,
269
+ f"{round(val * 100)}%")
270
+
271
+ def paintEvent(self, _e) -> None: # noqa: N802
272
+ t = self.theme.theme
273
+ p = QPainter(self)
274
+ p.setRenderHint(QPainter.Antialiasing)
275
+ f = self._f
276
+ if not f:
277
+ p.setPen(QColor(t.faint)); p.drawText(self.rect(), Qt.AlignCenter, "— no run —")
278
+ return
279
+ m = 14; w = self.width() - 2 * m
280
+ self._meter(p, m, m, w, "fault rate", f.get("fault_rate", 0), "red")
281
+ self._meter(p, m, m + 26, w, "locality", f.get("locality", 0), "green")
282
+ self._meter(p, m, m + 52, w, "working-set growth", min(1.0, f.get("ws_growth", 0)), "amber")
283
+ # frames vs working set, as a comparison line
284
+ y = m + 84
285
+ fr = f.get("frames", 0); ws = f.get("working_set", 0)
286
+ p.setPen(QColor(t.text))
287
+ p.drawText(m, y, w, 18, Qt.AlignLeft,
288
+ f"frames: {fr} working set: {ws} distinct pages: {f.get('unique_pages', 0)}")
289
+ # a little bar comparison: frames vs working set (scaled to the larger)
290
+ scale = max(fr, ws, 1)
291
+ p.setPen(Qt.NoPen)
292
+ p.setBrush(QColor(t.accent_for("blue")))
293
+ p.drawRoundedRect(m, y + 24, int((w) * fr / scale), 12, 4, 4)
294
+ p.setBrush(QColor(t.accent_for("purple")))
295
+ p.drawRoundedRect(m, y + 40, int((w) * ws / scale), 12, 4, 4)
296
+ p.setPen(QColor(t.muted))
297
+ p.drawText(m, y + 22, w, 12, Qt.AlignLeft, "")
298
+ p.drawText(m, y + 54, w, 14, Qt.AlignLeft, "blue = frames · purple = working set")
299
+
300
+
301
+ class EventCard(QWidget):
302
+ """A single trap/fault event as labeled tiles (the fault-type / trap-cause signature).
303
+ signature = dict of {field_label: value_string} in display order."""
304
+
305
+ def __init__(self, theme, parent=None) -> None:
306
+ super().__init__(parent)
307
+ self.theme = theme
308
+ self._fields: list = [] # [(label, value)]
309
+ self.setMinimumSize(320, 130)
310
+
311
+ def show_signature(self, fields) -> None:
312
+ if isinstance(fields, dict):
313
+ fields = list(fields.items())
314
+ self._fields = list(fields or [])
315
+ self.update()
316
+
317
+ def paintEvent(self, _e) -> None: # noqa: N802
318
+ t = self.theme.theme
319
+ p = QPainter(self)
320
+ p.setRenderHint(QPainter.Antialiasing)
321
+ if not self._fields:
322
+ p.setPen(QColor(t.faint))
323
+ p.drawText(self.rect(), Qt.AlignCenter, "— no event —")
324
+ return
325
+ m, gap = 14, 10
326
+ n = len(self._fields)
327
+ tw = (self.width() - 2 * m - gap * (n - 1)) / max(n, 1)
328
+ th = min(80, self.height() - 2 * m)
329
+ y = (self.height() - th) / 2
330
+ keys = ["blue", "purple", "amber", "green", "teal"]
331
+ for i, (label, value) in enumerate(self._fields):
332
+ x = m + i * (tw + gap)
333
+ col = QColor(t.accent_for(keys[i % len(keys)]))
334
+ p.setBrush(QColor(t.panel)); p.setPen(col)
335
+ p.drawRoundedRect(int(x), int(y), int(tw), int(th), 7, 7)
336
+ p.setPen(col)
337
+ p.drawText(int(x + 8), int(y + 6), int(tw - 12), 14, Qt.AlignLeft, str(label).upper())
338
+ p.setPen(QColor(t.text))
339
+ p.drawText(int(x + 8), int(y + 24), int(tw - 12), int(th - 30),
340
+ Qt.AlignLeft | Qt.TextWordWrap, str(value))
gini/ui/games_lab.py ADDED
@@ -0,0 +1,90 @@
1
+ """Games hub — lists every registered Diagnose game and opens the selected one.
2
+
3
+ A thin shell over game_catalog: a list of games on the left, the chosen DiagnoseGameWidget on the
4
+ right. Each Lab can also embed its own game directly (open_game), so this is the central launcher,
5
+ not the only door.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from PySide6.QtCore import Qt
10
+ from PySide6.QtWidgets import (
11
+ QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, QWidget,
12
+ )
13
+
14
+ from . import game_catalog
15
+ from .theme import ThemeManager, icons
16
+ from .theme.manager import scale_css as _scss
17
+
18
+
19
+ def open_game(parent, theme, device, state, game_id: str, live: bool):
20
+ """Open a single game (from a Lab's embedded 'Play' button) as its own window."""
21
+ entry = game_catalog.get(game_id)
22
+ if entry is None:
23
+ return None
24
+ w = entry.build(theme, state, live)
25
+ w.setWindowFlag(Qt.Window, True)
26
+ w.setWindowTitle(f"{entry.title} — {device.name}")
27
+ w.resize(760, 560)
28
+ w.show(); w.raise_()
29
+ return w
30
+
31
+
32
+ class GamesLab(QWidget):
33
+ def __init__(self, parent, theme: ThemeManager, device, state, live=False) -> None:
34
+ super().__init__(parent)
35
+ self.setWindowFlag(Qt.Window, True)
36
+ self.theme = theme
37
+ self.device = device
38
+ self.state = state
39
+ self.live = live
40
+ self._built: dict = {}
41
+
42
+ t = theme.theme
43
+ self.setWindowTitle(f"Games Lab — {device.name}")
44
+ self.resize(820, 600)
45
+ self.setStyleSheet(f"QWidget{{background:{t.bg};}}")
46
+ root = QVBoxLayout(self)
47
+
48
+ head = QHBoxLayout()
49
+ ic = QLabel(); ic.setPixmap(icons.render_pixmap("robot", t.accent_for("purple"), 22))
50
+ title = QLabel(f" Games Lab — {device.name}")
51
+ title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
52
+ head.addWidget(ic); head.addWidget(title); head.addStretch(1)
53
+ chip = QLabel("live" if live else "offline demo")
54
+ chip.setStyleSheet(f"color:{t.success if live else t.muted};background:{t.panel2};"
55
+ f"border:1px solid {t.line};border-radius:9px;padding:2px 10px;"
56
+ "font-size:11px;")
57
+ head.addWidget(chip)
58
+ root.addLayout(head)
59
+
60
+ body = QHBoxLayout()
61
+ self._list = QListWidget()
62
+ self._list.setFixedWidth(220)
63
+ self._list.setStyleSheet(
64
+ f"QListWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
65
+ "border-radius:8px;font-size:12px;padding:4px;}"
66
+ f"QListWidget::item{{padding:6px 4px;}}"
67
+ f"QListWidget::item:selected{{background:{t.panel2};color:{t.text};}}")
68
+ self._entries = game_catalog.catalog()
69
+ for e in self._entries:
70
+ QListWidgetItem(f"{e.title}\n{e.subtitle}", self._list)
71
+ self._list.currentRowChanged.connect(self._show)
72
+ body.addWidget(self._list)
73
+
74
+ self._stack = QStackedWidget()
75
+ self._stack.setStyleSheet(f"QStackedWidget{{border:1px solid {t.line};border-radius:8px;}}")
76
+ body.addWidget(self._stack, 1)
77
+ root.addLayout(body, 1)
78
+
79
+ if self._entries:
80
+ self._list.setCurrentRow(0)
81
+
82
+ def _show(self, row) -> None:
83
+ if not (0 <= row < len(self._entries)):
84
+ return
85
+ entry = self._entries[row]
86
+ if entry.id not in self._built:
87
+ w = entry.build(self.theme, self.state, self.live)
88
+ self._built[entry.id] = w
89
+ self._stack.addWidget(w)
90
+ self._stack.setCurrentWidget(self._built[entry.id])