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/storage_lab.py ADDED
@@ -0,0 +1,219 @@
1
+ """Storage Lab — the visual file-system workbench for an xv6 Machine.
2
+
3
+ Reads the FS state (domain.xv6_fs) and shows it four ways: the on-disk region MAP (boot | super
4
+ | log | inodes | bitmap | data), the INODES and the directory tree they realize, the BUFFER
5
+ CACHE with its hit rate, and the write-ahead LOG — where you can Simulate a write and watch a
6
+ transaction fill, commit, and install, which is how xv6 survives a crash. Renders from an
7
+ injected provider (offline DemoDisk here; a GDB-backed reader on the Mac later).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from PySide6.QtCore import Qt
12
+ from PySide6.QtGui import QColor, QPainter
13
+ from PySide6.QtWidgets import (
14
+ QDialog, QFrame, QGridLayout, QHBoxLayout, QHeaderView, QLabel, QPushButton, QTableWidget,
15
+ QTableWidgetItem, QVBoxLayout, QWidget,
16
+ )
17
+
18
+ from ..domain.xv6_fs import DemoDisk
19
+ from .theme import ThemeManager, icons
20
+ from .theme.manager import scale_css as _scss
21
+
22
+ _REGION_ACCENT = {"boot": "slate", "super": "blue", "log": "amber", "inodes": "green",
23
+ "bitmap": "purple", "data": "cyan"}
24
+ _ITYPE_ACCENT = {"dir": "blue", "file": "green", "dev": "amber", "free": "slate"}
25
+
26
+
27
+ class DiskStrip(QWidget):
28
+ """The on-disk region map — one labelled segment per region, widths weighted by block count
29
+ (with a floor so the tiny regions stay visible next to the huge data region)."""
30
+
31
+ def __init__(self, theme) -> None:
32
+ super().__init__()
33
+ self.theme = theme
34
+ self._regions = []
35
+ self.setMinimumHeight(60)
36
+
37
+ def set_regions(self, regions) -> None:
38
+ self._regions = list(regions)
39
+ self.update()
40
+
41
+ def paintEvent(self, _e) -> None:
42
+ p = QPainter(self)
43
+ t = self.theme.theme
44
+ p.fillRect(self.rect(), QColor(t.panel2))
45
+ if not self._regions:
46
+ return
47
+ floor = 0.06
48
+ raw = [max(r.count, 1) for r in self._regions]
49
+ tot = sum(raw)
50
+ weights = [max(v / tot, floor) for v in raw]
51
+ wsum = sum(weights)
52
+ x = 0.0
53
+ W = self.width()
54
+ for r, w in zip(self._regions, weights):
55
+ seg = W * (w / wsum)
56
+ col = QColor(t.accent_for(_REGION_ACCENT.get(r.name, "slate")))
57
+ p.fillRect(int(x), 6, int(seg) - 2, self.height() - 28, col)
58
+ p.setPen(QColor(t.text))
59
+ p.drawText(int(x) + 4, 22, r.name)
60
+ p.setPen(QColor(t.muted))
61
+ rng = f"{r.start}" if r.count <= 1 else f"{r.start}–{r.end}"
62
+ p.drawText(int(x) + 4, self.height() - 8, rng)
63
+ x += seg
64
+
65
+
66
+ class StorageLab(QDialog):
67
+ def __init__(self, parent, theme: ThemeManager, device=None, provider=None) -> None:
68
+ super().__init__(parent)
69
+ self.theme = theme
70
+ self.device = device
71
+ self.provider = provider or DemoDisk()
72
+
73
+ t = theme.theme
74
+ self.setWindowTitle(f"File System Lab — {getattr(device, 'name', 'xv6')}")
75
+ self.resize(920, 720)
76
+ self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
77
+ root = QVBoxLayout(self)
78
+ self._build_header(root)
79
+
80
+ self._strip = DiskStrip(theme)
81
+ root.addWidget(self._panel("On-disk layout · block regions", self._strip))
82
+
83
+ grid = QGridLayout(); grid.setSpacing(10); root.addLayout(grid, 1)
84
+ self._inode_tbl = self._table(["inum", "type", "nlink", "size", "blocks"])
85
+ grid.addWidget(self._panel("Inodes", self._inode_tbl, fill=True), 0, 0)
86
+ self._tree_tbl = self._table(["", "inum", "name"])
87
+ grid.addWidget(self._panel("Directory tree", self._tree_tbl, fill=True), 0, 1)
88
+ self._buf_tbl = self._table(["block", "valid", "dirty", "ref"])
89
+ self._buf_panel = self._panel("Buffer cache", self._buf_tbl, fill=True)
90
+ grid.addWidget(self._buf_panel, 1, 0)
91
+ grid.addWidget(self._build_log_panel(), 1, 1)
92
+ grid.setColumnStretch(0, 1); grid.setColumnStretch(1, 1)
93
+
94
+ self._render(self.provider.snapshot())
95
+
96
+ # -- header/panels ---------------------------------------------------- #
97
+ def _build_header(self, root) -> None:
98
+ t = self.theme.theme
99
+ head = QHBoxLayout()
100
+ ic = QLabel(); ic.setPixmap(icons.render_pixmap("database", t.accent_for("cyan"), 22))
101
+ title = QLabel(f" File System Lab — {getattr(self.device, 'name', 'xv6')}")
102
+ title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
103
+ head.addWidget(ic); head.addWidget(title); head.addStretch(1)
104
+ root.addLayout(head)
105
+ hint = QLabel("The on-disk regions, the inodes and the tree they build, the buffer "
106
+ "cache, and the write-ahead log that makes writes crash-safe.")
107
+ hint.setWordWrap(True); hint.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
108
+ root.addWidget(hint)
109
+
110
+ def _panel(self, title, inner, fill=False) -> QFrame:
111
+ t = self.theme.theme
112
+ f = QFrame(); f.setStyleSheet(
113
+ _scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
114
+ v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
115
+ h = QLabel(title); h.setStyleSheet(
116
+ _scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
117
+ v.addWidget(h)
118
+ inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
119
+ v.addWidget(inner, 1 if fill else 0)
120
+ return f
121
+
122
+ def _table(self, cols) -> QTableWidget:
123
+ t = self.theme.theme
124
+ tbl = QTableWidget(0, len(cols))
125
+ tbl.setHorizontalHeaderLabels(cols)
126
+ tbl.verticalHeader().setVisible(False)
127
+ tbl.setEditTriggers(QTableWidget.NoEditTriggers)
128
+ tbl.setSelectionMode(QTableWidget.NoSelection)
129
+ tbl.horizontalHeader().setSectionResizeMode(len(cols) - 1, QHeaderView.Stretch)
130
+ tbl.setStyleSheet(
131
+ f"QTableWidget{{background:{t.panel};color:{t.text};border:none;"
132
+ f"gridline-color:{t.line};font-size:12px;}}"
133
+ f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
134
+ "padding:4px;}")
135
+ return tbl
136
+
137
+ def _build_log_panel(self) -> QFrame:
138
+ t = self.theme.theme
139
+ f = QFrame(); f.setStyleSheet(
140
+ _scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
141
+ v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
142
+ top = QHBoxLayout()
143
+ h = QLabel("Write-ahead log · journal"); h.setStyleSheet(
144
+ _scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
145
+ top.addWidget(h); top.addStretch(1)
146
+ self._log_phase = QLabel(); self._log_phase.setStyleSheet(
147
+ f"color:{t.text};background:{t.panel};border:1px solid {t.line};"
148
+ "border-radius:9px;padding:2px 10px;font-size:11px;")
149
+ top.addWidget(self._log_phase)
150
+ v.addLayout(top)
151
+ self._log_body = QLabel(); self._log_body.setWordWrap(True); self._log_body.setAlignment(Qt.AlignTop)
152
+ self._log_body.setStyleSheet(_scss(f"color:{t.text};font-family:monospace;font-size:12px;border:none;"))
153
+ v.addWidget(self._log_body, 1)
154
+ live = not hasattr(self.provider, "simulate_write") # live reader can't fake a txn
155
+ btn = QPushButton(" Refresh" if live else " Simulate write")
156
+ btn.setToolTip("Re-read the live file-system state (launch `writer` from the scheduler "
157
+ "window to make real log transactions)" if live else
158
+ "Advance a simulated write-ahead-log transaction")
159
+ btn.setIcon(icons.icon("save", t.accent_for("green"), 14))
160
+ btn.clicked.connect(self._on_write)
161
+ btn.setStyleSheet(
162
+ f"QPushButton{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
163
+ f"border-radius:8px;padding:6px 12px;}}QPushButton:hover{{border-color:{t.accent};}}")
164
+ v.addWidget(btn)
165
+ return f
166
+
167
+ # -- actions ---------------------------------------------------------- #
168
+ def _on_write(self) -> None:
169
+ fn = getattr(self.provider, "simulate_write", None)
170
+ self._render(fn() if callable(fn) else self.provider.snapshot())
171
+
172
+ def _render(self, snap) -> None:
173
+ t = self.theme.theme
174
+ self._strip.set_regions(snap.regions)
175
+ # inodes
176
+ self._inode_tbl.setRowCount(len(snap.inodes))
177
+ for r, ino in enumerate(snap.inodes):
178
+ vals = [str(ino.inum), ino.type, str(ino.nlink), str(ino.size), str(len(ino.blocks))]
179
+ for c, val in enumerate(vals):
180
+ it = QTableWidgetItem(val)
181
+ if c == 1:
182
+ it.setForeground(QColor(t.accent_for(_ITYPE_ACCENT.get(ino.type, "slate"))))
183
+ self._inode_tbl.setItem(r, c, it)
184
+ # tree
185
+ self._tree_tbl.setRowCount(len(snap.tree))
186
+ for r, d in enumerate(snap.tree):
187
+ leaf = d.name.split("/")[-1]
188
+ name = (" " * d.depth) + leaf + ("/" if d.is_dir else "")
189
+ for c, val in enumerate(["", str(d.inum), name]):
190
+ self._tree_tbl.setItem(r, c, QTableWidgetItem(val))
191
+ # buffer cache
192
+ self._buf_tbl.setRowCount(len(snap.bufs))
193
+ for r, b in enumerate(snap.bufs):
194
+ for c, val in enumerate([str(b.blockno), "✓" if b.valid else "",
195
+ "●" if b.dirty else "", str(b.refcnt)]):
196
+ it = QTableWidgetItem(val)
197
+ if c == 2 and b.dirty:
198
+ it.setForeground(QColor(t.accent_for("amber")))
199
+ self._buf_tbl.setItem(r, c, it)
200
+ self._buf_panel.findChild(QLabel).setText(
201
+ f"Buffer cache · {snap.hits} hits / {snap.misses} miss "
202
+ f"({snap.hit_rate * 100:.0f}% hit)")
203
+ # log
204
+ lg = snap.log
205
+ self._log_phase.setText(lg.phase)
206
+ col = {"idle": t.muted, "building": t.accent_for("amber"),
207
+ "committing": t.accent_for("green")}.get(lg.phase, t.muted)
208
+ self._log_phase.setStyleSheet(
209
+ f"color:{col};background:{t.panel};border:1px solid {t.line};"
210
+ "border-radius:9px;padding:2px 10px;font-size:11px;")
211
+ if lg.blocks:
212
+ self._log_body.setText(
213
+ f"transaction: {len(lg.blocks)} block(s) staged in the log\n"
214
+ f"dest blocks: {', '.join(map(str, lg.blocks))}\n"
215
+ + ("committing → installing to their home locations…" if lg.committing
216
+ else "logged but not yet committed (a crash here loses nothing)."))
217
+ else:
218
+ self._log_body.setText("log idle — no transaction in flight.\n"
219
+ "Press “Simulate write” to stage one and watch it commit.")
@@ -0,0 +1,235 @@
1
+ """Syscall Builder — a visual front-end for adding a real system call to xv6.
2
+
3
+ The student names the call, declares its arguments and return type, and drops in a C body;
4
+ GINI generates the five real xv6 edits (see domain.syscall_builder) and previews them. Applying
5
+ writes the patches and recompiles xv6 — that step is the desktop (Mac) build; offline the dialog
6
+ still generates and previews the exact code, so the whole authoring experience is usable and
7
+ testable without a compiler.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from PySide6.QtCore import Qt
12
+ from PySide6.QtWidgets import (
13
+ QComboBox, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPlainTextEdit,
14
+ QPushButton, QTableWidget, QVBoxLayout, QWidget,
15
+ )
16
+
17
+ from ..domain import syscall_builder as sb
18
+ from .theme import ThemeManager, icons
19
+ from .theme.manager import scale_css as _scss
20
+
21
+ _ARG_LABELS = [("int", "int"), ("pointer (addr)", "addr"), ("string", "str")]
22
+
23
+
24
+ class SyscallBuilder(QDialog):
25
+ def __init__(self, parent, theme: ThemeManager, device=None, on_apply=None,
26
+ existing=None) -> None:
27
+ super().__init__(parent)
28
+ self.theme = theme
29
+ self.device = device
30
+ self.on_apply = on_apply # callable(Codegen) — Mac writes+recompiles
31
+ self._existing = tuple(existing or sb.STOCK_SYSCALLS)
32
+ self._added = 0 # syscalls authored this session (for numbering)
33
+ self._codegen = None
34
+
35
+ t = theme.theme
36
+ self.setWindowTitle("Syscall Builder — xv6")
37
+ self.resize(940, 680)
38
+ self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
39
+ root = QVBoxLayout(self)
40
+ self._build_header(root)
41
+ body = QHBoxLayout(); root.addLayout(body, 1)
42
+ body.addWidget(self._build_form(), 0)
43
+ body.addWidget(self._build_preview(), 1)
44
+ self._build_footer(root)
45
+ self._add_arg_row("n", "int")
46
+
47
+ # -- header ----------------------------------------------------------- #
48
+ def _build_header(self, root) -> None:
49
+ t = self.theme.theme
50
+ head = QHBoxLayout()
51
+ ic = QLabel(); ic.setPixmap(icons.render_pixmap("compile", t.accent_for("red"), 22))
52
+ title = QLabel(" Add a system call to xv6")
53
+ title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
54
+ head.addWidget(ic); head.addWidget(title); head.addStretch(1)
55
+ root.addLayout(head)
56
+ hint = QLabel("Declare it, write the C body, and GINI generates the five real xv6 "
57
+ "edits. Applying recompiles the kernel (desktop build).")
58
+ hint.setWordWrap(True); hint.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
59
+ root.addWidget(hint)
60
+
61
+ def _panel(self, title) -> tuple[QFrame, QVBoxLayout]:
62
+ t = self.theme.theme
63
+ f = QFrame(); f.setStyleSheet(
64
+ _scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
65
+ v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
66
+ h = QLabel(title); h.setStyleSheet(
67
+ _scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
68
+ v.addWidget(h)
69
+ return f, v
70
+
71
+ # -- the form --------------------------------------------------------- #
72
+ def _build_form(self) -> QFrame:
73
+ t = self.theme.theme
74
+ f, v = self._panel("Declaration")
75
+ f.setFixedWidth(380)
76
+
77
+ row = QHBoxLayout()
78
+ row.addWidget(self._lbl("name"))
79
+ self.name_edit = QLineEdit(); self.name_edit.setPlaceholderText("e.g. trace, hello")
80
+ self.name_edit.setStyleSheet(self._edit_css())
81
+ self.name_edit.textChanged.connect(self._on_name_changed)
82
+ row.addWidget(self.name_edit, 1)
83
+ row.addWidget(self._lbl("returns"))
84
+ self.ret_combo = QComboBox(); self.ret_combo.addItems(list(sb.RET_TYPES))
85
+ self.ret_combo.setStyleSheet(self._edit_css())
86
+ row.addWidget(self.ret_combo)
87
+ v.addLayout(row)
88
+
89
+ v.addWidget(self._lbl("arguments (up to 6)"))
90
+ self.args_tbl = QTableWidget(0, 2)
91
+ self.args_tbl.setHorizontalHeaderLabels(["name", "type"])
92
+ self.args_tbl.verticalHeader().setVisible(False)
93
+ self.args_tbl.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
94
+ self.args_tbl.setStyleSheet(
95
+ f"QTableWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
96
+ f"gridline-color:{t.line};font-size:12px;}}"
97
+ f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
98
+ "padding:4px;}")
99
+ v.addWidget(self.args_tbl, 1)
100
+ arow = QHBoxLayout()
101
+ add = QPushButton(" + arg"); add.setStyleSheet(self._btn_css())
102
+ add.clicked.connect(lambda: self._add_arg_row())
103
+ rm = QPushButton(" − arg"); rm.setStyleSheet(self._btn_css())
104
+ rm.clicked.connect(self._remove_arg_row)
105
+ arow.addWidget(add); arow.addWidget(rm); arow.addStretch(1)
106
+ v.addLayout(arow)
107
+
108
+ v.addWidget(self._lbl("C body (runs in the kernel)"))
109
+ self.body_edit = QPlainTextEdit(sb.starter_body())
110
+ self.body_edit.setStyleSheet(
111
+ f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
112
+ "border-radius:6px;font-family:monospace;font-size:12px;}")
113
+ self.body_edit.setFixedHeight(150)
114
+ v.addWidget(self.body_edit)
115
+ return f
116
+
117
+ def _build_preview(self) -> QFrame:
118
+ t = self.theme.theme
119
+ f, v = self._panel("Generated xv6 edits (5 files)")
120
+ self.preview = QPlainTextEdit(); self.preview.setReadOnly(True)
121
+ self.preview.setPlaceholderText("Press Generate to see the code GINI will add.")
122
+ self.preview.setStyleSheet(
123
+ f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
124
+ "border-radius:6px;font-family:monospace;font-size:12px;}")
125
+ v.addWidget(self.preview, 1)
126
+ return f
127
+
128
+ def _build_footer(self, root) -> None:
129
+ t = self.theme.theme
130
+ bar = QHBoxLayout()
131
+ self.status = QLabel("")
132
+ self.status.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
133
+ bar.addWidget(self.status, 1)
134
+ gen = QPushButton(" Generate"); gen.setIcon(icons.icon("compile", t.accent_for("blue"), 14))
135
+ gen.setStyleSheet(self._btn_css()); gen.clicked.connect(self._on_generate)
136
+ bar.addWidget(gen)
137
+ self.apply_btn = QPushButton(" Apply & recompile")
138
+ self.apply_btn.setIcon(icons.icon("save", t.accent_for("green"), 14))
139
+ self.apply_btn.setStyleSheet(self._btn_css()); self.apply_btn.setEnabled(False)
140
+ self.apply_btn.clicked.connect(self._on_apply)
141
+ bar.addWidget(self.apply_btn)
142
+ root.addLayout(bar)
143
+
144
+ # -- helpers ---------------------------------------------------------- #
145
+ def _lbl(self, text) -> QLabel:
146
+ lb = QLabel(text)
147
+ lb.setStyleSheet(_scss(f"color:{self.theme.theme.muted};font-size:11px;border:none;"))
148
+ return lb
149
+
150
+ def _edit_css(self) -> str:
151
+ t = self.theme.theme
152
+ return (f"QLineEdit,QComboBox{{background:{t.panel};color:{t.text};"
153
+ f"border:1px solid {t.line};border-radius:6px;padding:4px 6px;}}")
154
+
155
+ def _btn_css(self) -> str:
156
+ t = self.theme.theme
157
+ return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
158
+ f"border-radius:8px;padding:6px 12px;}}"
159
+ f"QPushButton:hover{{border-color:{t.accent};}}"
160
+ f"QPushButton:disabled{{color:{t.faint};}}")
161
+
162
+ def _add_arg_row(self, name="", typ="int") -> None:
163
+ if self.args_tbl.rowCount() >= 6:
164
+ return
165
+ r = self.args_tbl.rowCount()
166
+ self.args_tbl.insertRow(r)
167
+ edit = QLineEdit(name); edit.setStyleSheet(self._edit_css())
168
+ self.args_tbl.setCellWidget(r, 0, edit)
169
+ combo = QComboBox(); combo.setStyleSheet(self._edit_css())
170
+ for label, val in _ARG_LABELS:
171
+ combo.addItem(label, val)
172
+ combo.setCurrentIndex(next((i for i, (_l, v) in enumerate(_ARG_LABELS) if v == typ), 0))
173
+ self.args_tbl.setCellWidget(r, 1, combo)
174
+
175
+ def _remove_arg_row(self) -> None:
176
+ n = self.args_tbl.rowCount()
177
+ if n:
178
+ self.args_tbl.removeRow(n - 1)
179
+
180
+ def _on_name_changed(self, text) -> None:
181
+ # keep the starter body's printf in sync with the name until the user edits it
182
+ if self.body_edit.toPlainText().strip().startswith("// read your args"):
183
+ self.body_edit.setPlainText(sb.starter_body(text))
184
+
185
+ # -- spec + actions --------------------------------------------------- #
186
+ def spec_from_form(self) -> sb.SyscallSpec:
187
+ args = []
188
+ for r in range(self.args_tbl.rowCount()):
189
+ nm = self.args_tbl.cellWidget(r, 0).text().strip()
190
+ tp = self.args_tbl.cellWidget(r, 1).currentData()
191
+ if nm:
192
+ args.append(sb.SyscallArg(nm, tp))
193
+ return sb.SyscallSpec(name=self.name_edit.text().strip(),
194
+ ret=self.ret_combo.currentText(),
195
+ args=args, body=self.body_edit.toPlainText())
196
+
197
+ def _on_generate(self) -> None:
198
+ t = self.theme.theme
199
+ spec = self.spec_from_form()
200
+ errs = sb.validate(spec, existing=self._existing)
201
+ if errs:
202
+ self._codegen = None
203
+ self.apply_btn.setEnabled(False)
204
+ self.preview.setPlainText("")
205
+ self.status.setText("⚠ " + errs[0])
206
+ self.status.setStyleSheet(_scss(f"color:{t.danger};font-size:12px;"))
207
+ return
208
+ cg = sb.generate(spec, number=sb.NEXT_FREE_NUMBER + self._added)
209
+ self._codegen = cg
210
+ self.preview.setPlainText(cg.preview())
211
+ self.apply_btn.setEnabled(True)
212
+ self.status.setText(f"✓ Ready — SYS_{spec.name} = {cg.number}. "
213
+ "Review the 5 edits, then Apply.")
214
+ self.status.setStyleSheet(_scss(f"color:{t.success};font-size:12px;"))
215
+
216
+ def _on_apply(self) -> None:
217
+ t = self.theme.theme
218
+ if self._codegen is None:
219
+ return
220
+ if self.on_apply is not None:
221
+ try:
222
+ name = self.spec_from_form().name
223
+ self.on_apply(self._codegen)
224
+ self._added += 1
225
+ self._existing = self._existing + (name,) # block re-using this name
226
+ self.status.setText("Applied — writing the 5 edits and recompiling xv6…")
227
+ self.status.setStyleSheet(_scss(f"color:{t.success};font-size:12px;"))
228
+ self.apply_btn.setEnabled(False)
229
+ except Exception as e:
230
+ self.status.setText(f"⚠ Apply failed: {e}")
231
+ self.status.setStyleSheet(_scss(f"color:{t.danger};font-size:12px;"))
232
+ else:
233
+ self.status.setText("Applying writes these 5 edits and recompiles xv6 — available "
234
+ "on the desktop build. The generated code is shown at left.")
235
+ self.status.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
gini/ui/syscall_lab.py ADDED
@@ -0,0 +1,152 @@
1
+ """Syscall Lab — live histogram of system-call activity + an strace-style trace.
2
+
3
+ Reads gini_scdump over the serial (the agent's /sc, no gdb halt): per-syscall counters drive a
4
+ histogram of calls in the last 60 seconds (so you see what the machine is doing under the hood),
5
+ and a recent-call ring drives a running trace (`pid name(arg) = ret`). User-defined syscalls from
6
+ the Builder appear automatically, since counters are keyed by syscall number.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from PySide6.QtCore import Qt, QTimer, Signal
11
+ from PySide6.QtGui import QColor, QPainter
12
+ from PySide6.QtWidgets import (
13
+ QDialog, QFrame, QHBoxLayout, QLabel, QPlainTextEdit, QVBoxLayout, QWidget,
14
+ )
15
+
16
+ from ..domain.xv6 import SyscallRate, parse_sccounts, parse_sctrace, syscall_name
17
+ from .theme import ThemeManager, icons
18
+
19
+
20
+ class HistogramBars(QWidget):
21
+ """Horizontal bars for the top syscalls by call count in the window."""
22
+
23
+ def __init__(self, theme) -> None:
24
+ super().__init__()
25
+ self.theme = theme
26
+ self._rows: list = [] # [(label, value)]
27
+ self.setMinimumHeight(220)
28
+
29
+ def set_data(self, rows) -> None:
30
+ self._rows = list(rows)
31
+ self.update()
32
+
33
+ def paintEvent(self, _e) -> None:
34
+ p = QPainter(self)
35
+ t = self.theme.theme
36
+ p.fillRect(self.rect(), QColor(t.panel))
37
+ if not self._rows:
38
+ p.setPen(QColor(t.muted))
39
+ p.drawText(self.rect(), Qt.AlignCenter, "no syscalls yet — launch a program")
40
+ return
41
+ top = self._rows[:12]
42
+ peak = max((v for _l, v in top), default=1) or 1
43
+ rowh = min(24, max(16, (self.height() - 8) // max(len(top), 1)))
44
+ label_w, count_w = 92, 52
45
+ bar_x = label_w
46
+ bar_max = max(40, self.width() - label_w - count_w - 12)
47
+ y = 4
48
+ for label, v in top:
49
+ p.setPen(QColor(t.text))
50
+ p.drawText(4, y, label_w - 6, rowh, Qt.AlignVCenter | Qt.AlignLeft, label)
51
+ w = int(bar_max * (v / peak))
52
+ p.fillRect(bar_x, y + 3, max(w, 1), rowh - 8, QColor(t.accent_for("blue")))
53
+ p.setPen(QColor(t.muted))
54
+ p.drawText(bar_x + bar_max + 6, y, count_w, rowh, Qt.AlignVCenter | Qt.AlignLeft,
55
+ str(v))
56
+ y += rowh
57
+
58
+
59
+ class SyscallLab(QDialog):
60
+ sc_ready = Signal(str) # raw /sc text pushed from the poll worker
61
+
62
+ def __init__(self, parent, theme: ThemeManager, device=None, sc_source=None,
63
+ name_extra=None) -> None:
64
+ super().__init__(parent)
65
+ self.theme = theme
66
+ self.device = device
67
+ self._sc_source = sc_source or (lambda: "")
68
+ self._name_extra = name_extra or {}
69
+ self._rate = SyscallRate(window=60.0)
70
+ self._busy = False
71
+
72
+ t = theme.theme
73
+ self.setWindowTitle(f"System Calls Lab — {getattr(device, 'name', 'xv6')}")
74
+ self.resize(760, 560)
75
+ self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
76
+ root = QVBoxLayout(self)
77
+
78
+ hdr = QHBoxLayout()
79
+ ic = QLabel(); ic.setPixmap(icons.render_pixmap("send", t.accent_for("blue"), 22))
80
+ title = QLabel(f" System Calls Lab — {getattr(device, 'name', 'xv6')}")
81
+ title.setStyleSheet(f"color:{t.text};font-size:16px;font-weight:600;")
82
+ hdr.addWidget(ic); hdr.addWidget(title); hdr.addStretch(1)
83
+ root.addLayout(hdr)
84
+
85
+ head = QLabel("What the kernel is doing under the hood — system calls in the last 60s "
86
+ "(histogram) and the most recent calls (trace). No gdb; live over serial.")
87
+ head.setWordWrap(True); head.setStyleSheet(f"color:{t.muted};font-size:12px;")
88
+ root.addWidget(head)
89
+
90
+ self._bars = HistogramBars(theme)
91
+ root.addWidget(self._panel("Syscalls · calls in last 60s", self._bars), 1)
92
+
93
+ self._trace = QPlainTextEdit(); self._trace.setReadOnly(True)
94
+ self._trace.setStyleSheet(
95
+ f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
96
+ "border-radius:6px;font-family:monospace;font-size:12px;}")
97
+ root.addWidget(self._panel("Trace · recent calls (pid name(arg) = ret)", self._trace), 1)
98
+
99
+ self.sc_ready.connect(self._apply)
100
+ self._poll = QTimer(self); self._poll.timeout.connect(self._fetch)
101
+ self._poll.start(1500)
102
+ self._fetch()
103
+
104
+ def _panel(self, title, inner) -> QFrame:
105
+ t = self.theme.theme
106
+ f = QFrame(); f.setStyleSheet(
107
+ f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}")
108
+ v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
109
+ h = QLabel(title); h.setStyleSheet(
110
+ f"color:{t.muted};font-size:11px;font-weight:600;border:none;")
111
+ v.addWidget(h)
112
+ inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
113
+ v.addWidget(inner, 1)
114
+ return f
115
+
116
+ def _fetch(self) -> None:
117
+ if self._busy:
118
+ return
119
+ self._busy = True
120
+ import threading
121
+
122
+ def work():
123
+ try:
124
+ txt = self._sc_source()
125
+ except Exception:
126
+ txt = ""
127
+ self.sc_ready.emit(txt or "")
128
+ threading.Thread(target=work, daemon=True).start()
129
+
130
+ def _apply(self, txt) -> None:
131
+ self._busy = False
132
+ import time
133
+ counts = parse_sccounts(txt)
134
+ if counts:
135
+ self._rate.add(time.monotonic(), counts)
136
+ rates = self._rate.rates()
137
+ rows = sorted(((syscall_name(n, self._name_extra), v) for n, v in rates.items()),
138
+ key=lambda kv: -kv[1])
139
+ self._bars.set_data(rows)
140
+ evs = parse_sctrace(txt)
141
+ if evs:
142
+ lines = [f"{e.pid:>3} {syscall_name(e.num, self._name_extra)}({e.a0}) = {e.ret}"
143
+ for e in evs]
144
+ sb = self._trace.verticalScrollBar()
145
+ at_bottom = sb.value() >= sb.maximum() - 4
146
+ self._trace.setPlainText("\n".join(lines))
147
+ if at_bottom:
148
+ sb.setValue(sb.maximum())
149
+
150
+ def closeEvent(self, e) -> None: # noqa: N802
151
+ self._poll.stop()
152
+ super().closeEvent(e)
@@ -0,0 +1,5 @@
1
+ from . import icons
2
+ from .manager import ThemeManager, build_qss
3
+ from .tokens import THEMES, Theme, get_theme
4
+
5
+ __all__ = ["icons", "ThemeManager", "build_qss", "THEMES", "Theme", "get_theme"]