gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/trap_lab.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Trap Lab — live trap-taxonomy histogram + a running feed of individual traps.
|
|
2
|
+
|
|
3
|
+
Reads gini_trapdump over the serial (the agent's /traps, no gdb halt): per-kind counters drive a
|
|
4
|
+
histogram of traps in the last 60s (syscall / page-fault / timer / device / illegal / other), and
|
|
5
|
+
a ring drives a live feed (pid · kind · epc · faulting-address). The lesson students take away is
|
|
6
|
+
that a system call, a timer preemption, and a demand-paged page are the SAME mechanism with
|
|
7
|
+
different causes. "Step a trap ▸" opens the CPU journey to dissect one frame by frame.
|
|
8
|
+
|
|
9
|
+
Same shape as the Syscall Lab: an injected `traps_source` (the bridge's /traps live, or
|
|
10
|
+
DemoScheduler.traps() offline) polled off the GUI thread, so the whole face is testable against a
|
|
11
|
+
fake feed.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from PySide6.QtCore import Qt, QTimer, Signal
|
|
16
|
+
from PySide6.QtGui import QColor, QPainter
|
|
17
|
+
from PySide6.QtWidgets import (
|
|
18
|
+
QComboBox, QDialog, QFrame, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton, QVBoxLayout,
|
|
19
|
+
QWidget,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ..domain.xv6 import (
|
|
23
|
+
TRAP_KINDS, TrapRate, parse_alarms, parse_trapcounts, parse_traptrace, trap_kind_name,
|
|
24
|
+
)
|
|
25
|
+
from .theme import ThemeManager, icons
|
|
26
|
+
|
|
27
|
+
# trap kinds the "Step a trap" catcher can target (conditioned gdb breakpoint); "any" = next trap
|
|
28
|
+
_CATCH_KINDS = ["any", "pagefault", "syscall", "timer", "illegal", "device"]
|
|
29
|
+
|
|
30
|
+
# one colour per trap kind, so the histogram reads at a glance
|
|
31
|
+
_KIND_ACCENT = {0: "blue", 1: "purple", 2: "amber", 3: "cyan", 4: "red", 5: "slate"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TrapBars(QWidget):
|
|
35
|
+
"""Horizontal bars for the six trap kinds, one colour per kind."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, theme) -> None:
|
|
38
|
+
super().__init__()
|
|
39
|
+
self.theme = theme
|
|
40
|
+
self._rows: list = [] # [(kind_index, label, value)]
|
|
41
|
+
self.setMinimumHeight(180)
|
|
42
|
+
|
|
43
|
+
def set_data(self, rows) -> None:
|
|
44
|
+
self._rows = list(rows)
|
|
45
|
+
self.update()
|
|
46
|
+
|
|
47
|
+
def paintEvent(self, _e) -> None:
|
|
48
|
+
p = QPainter(self)
|
|
49
|
+
t = self.theme.theme
|
|
50
|
+
p.fillRect(self.rect(), QColor(t.panel))
|
|
51
|
+
if not self._rows or not any(v for _k, _l, v in self._rows):
|
|
52
|
+
p.setPen(QColor(t.muted))
|
|
53
|
+
p.drawText(self.rect(), Qt.AlignCenter, "no traps yet — run the machine (or use Demo)")
|
|
54
|
+
return
|
|
55
|
+
peak = max((v for _k, _l, v in self._rows), default=1) or 1
|
|
56
|
+
rowh = min(26, max(18, (self.height() - 8) // max(len(self._rows), 1)))
|
|
57
|
+
label_w, count_w = 96, 56
|
|
58
|
+
bar_max = max(40, self.width() - label_w - count_w - 12)
|
|
59
|
+
y = 4
|
|
60
|
+
for kind, label, v in self._rows:
|
|
61
|
+
p.setPen(QColor(t.text))
|
|
62
|
+
p.drawText(4, y, label_w - 6, rowh, Qt.AlignVCenter | Qt.AlignLeft, label)
|
|
63
|
+
w = int(bar_max * (v / peak))
|
|
64
|
+
p.fillRect(label_w, y + 3, max(w, 1), rowh - 8,
|
|
65
|
+
QColor(t.accent_for(_KIND_ACCENT.get(kind, "blue"))))
|
|
66
|
+
p.setPen(QColor(t.muted))
|
|
67
|
+
p.drawText(label_w + bar_max + 6, y, count_w, rowh,
|
|
68
|
+
Qt.AlignVCenter | Qt.AlignLeft, str(v))
|
|
69
|
+
y += rowh
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TrapLab(QDialog):
|
|
73
|
+
traps_ready = Signal(str) # raw /traps text pushed from the poll worker
|
|
74
|
+
alarms_ready = Signal(str) # raw gini_dump text (ALARM lines) from the poll worker
|
|
75
|
+
caught = Signal(object) # a TrapFrame from a live catch (or None), off the worker
|
|
76
|
+
|
|
77
|
+
def __init__(self, parent, theme: ThemeManager, device=None, traps_source=None,
|
|
78
|
+
on_step=None, catch_source=None, alarm_source=None, on_play=None) -> None:
|
|
79
|
+
super().__init__(parent)
|
|
80
|
+
self.theme = theme
|
|
81
|
+
self.device = device
|
|
82
|
+
self._src = traps_source or (lambda: "")
|
|
83
|
+
self._on_step = on_step
|
|
84
|
+
self._on_play = on_play # callable() opening the decode-the-trap game; may be None
|
|
85
|
+
self._catch = catch_source # callable(kind) -> TrapFrame (live gdb freeze); may be None
|
|
86
|
+
self._alarm_src = alarm_source # callable() -> gini_dump text with ALARM lines; may be None
|
|
87
|
+
self._rate = TrapRate(window=60.0)
|
|
88
|
+
self._busy = False
|
|
89
|
+
self._closed = False
|
|
90
|
+
|
|
91
|
+
t = theme.theme
|
|
92
|
+
self.setWindowTitle(f"Traps & Interrupts Lab — {getattr(device, 'name', 'xv6')}")
|
|
93
|
+
self.resize(760, 580)
|
|
94
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
95
|
+
root = QVBoxLayout(self)
|
|
96
|
+
|
|
97
|
+
hdr = QHBoxLayout()
|
|
98
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("dashboard", t.accent_for("amber"), 22))
|
|
99
|
+
title = QLabel(f" Traps & Interrupts Lab — {getattr(device, 'name', 'xv6')}")
|
|
100
|
+
title.setStyleSheet(f"color:{t.text};font-size:16px;font-weight:600;")
|
|
101
|
+
hdr.addWidget(ic); hdr.addWidget(title); hdr.addStretch(1)
|
|
102
|
+
root.addLayout(hdr)
|
|
103
|
+
|
|
104
|
+
head = QLabel("Every trap the CPU takes, classified by cause. A system call, a timer "
|
|
105
|
+
"preemption, and a demand-paged page are the same mechanism — watch the mix "
|
|
106
|
+
"as programs run. No gdb; live over serial.")
|
|
107
|
+
head.setWordWrap(True); head.setStyleSheet(f"color:{t.muted};font-size:12px;")
|
|
108
|
+
root.addWidget(head)
|
|
109
|
+
|
|
110
|
+
self._bars = TrapBars(theme)
|
|
111
|
+
root.addWidget(self._panel("Traps · by cause, last 60s", self._bars), 1)
|
|
112
|
+
|
|
113
|
+
self._feed = QPlainTextEdit(); self._feed.setReadOnly(True)
|
|
114
|
+
self._feed.setStyleSheet(
|
|
115
|
+
f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
116
|
+
"border-radius:6px;font-family:monospace;font-size:12px;}")
|
|
117
|
+
root.addWidget(self._panel("Feed · recent traps (pid kind epc addr)", self._feed), 1)
|
|
118
|
+
|
|
119
|
+
# the sigalarm-lab strip — only shown when an alarm source is wired (Phase 3). It's the
|
|
120
|
+
# live proof a student's periodic handler works: the countdown ticks, `on` flips on fire.
|
|
121
|
+
self._alarms = QLabel()
|
|
122
|
+
self._alarms.setWordWrap(True)
|
|
123
|
+
self._alarms.setStyleSheet(
|
|
124
|
+
f"color:{t.text};background:{t.panel2};border:1px solid {t.line};border-radius:8px;"
|
|
125
|
+
"padding:6px 10px;font-family:monospace;font-size:12px;")
|
|
126
|
+
self._alarms.setVisible(self._alarm_src is not None)
|
|
127
|
+
root.addWidget(self._alarms)
|
|
128
|
+
|
|
129
|
+
row = QHBoxLayout()
|
|
130
|
+
if self._on_play is not None: # in-lab game: decode the trap from its scause
|
|
131
|
+
play = QPushButton(" Play: decode the trap")
|
|
132
|
+
play.setStyleSheet(
|
|
133
|
+
f"QPushButton{{color:{t.accent_for('purple')};background:{t.panel2};"
|
|
134
|
+
f"border:1px solid {t.line};border-radius:8px;padding:6px 12px;}}"
|
|
135
|
+
f"QPushButton:hover{{border-color:{t.accent};}}")
|
|
136
|
+
play.clicked.connect(lambda: self._on_play())
|
|
137
|
+
row.addWidget(play)
|
|
138
|
+
row.addStretch(1)
|
|
139
|
+
self._kind = QComboBox(); self._kind.addItems(_CATCH_KINDS)
|
|
140
|
+
self._kind.setToolTip("Which kind of trap to freeze")
|
|
141
|
+
self._kind.setStyleSheet(
|
|
142
|
+
f"QComboBox{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
143
|
+
"border-radius:6px;padding:4px 8px;}")
|
|
144
|
+
catch_lbl = QLabel("catch:"); catch_lbl.setStyleSheet(f"color:{t.muted};font-size:12px;")
|
|
145
|
+
row.addWidget(catch_lbl)
|
|
146
|
+
row.addWidget(self._kind)
|
|
147
|
+
self._step_btn = QPushButton(" Step a trap ▸")
|
|
148
|
+
self._step_btn.setToolTip("Freeze a real trap of this kind and dissect it in the CPU journey")
|
|
149
|
+
self._step_btn.setStyleSheet(
|
|
150
|
+
f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
151
|
+
f"border-radius:8px;padding:6px 12px;}}QPushButton:hover{{border-color:{t.accent};}}")
|
|
152
|
+
self._step_btn.clicked.connect(self._step)
|
|
153
|
+
row.addWidget(self._step_btn)
|
|
154
|
+
root.addLayout(row)
|
|
155
|
+
|
|
156
|
+
self.traps_ready.connect(self._apply)
|
|
157
|
+
self.alarms_ready.connect(self._apply_alarms)
|
|
158
|
+
self.caught.connect(self._on_caught)
|
|
159
|
+
self._poll = QTimer(self); self._poll.timeout.connect(self._fetch)
|
|
160
|
+
self._poll.start(1500)
|
|
161
|
+
self._fetch()
|
|
162
|
+
|
|
163
|
+
def _panel(self, title, inner) -> QFrame:
|
|
164
|
+
t = self.theme.theme
|
|
165
|
+
f = QFrame(); f.setStyleSheet(
|
|
166
|
+
f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}")
|
|
167
|
+
v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
|
|
168
|
+
h = QLabel(title); h.setStyleSheet(
|
|
169
|
+
f"color:{t.muted};font-size:11px;font-weight:600;border:none;")
|
|
170
|
+
v.addWidget(h)
|
|
171
|
+
inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
|
|
172
|
+
v.addWidget(inner, 1)
|
|
173
|
+
return f
|
|
174
|
+
|
|
175
|
+
def _step(self) -> None:
|
|
176
|
+
"""Freeze a live trap (off the GUI thread — the gdb catch can take up to a few seconds),
|
|
177
|
+
then open the journey seeded with it. With no live catch source, open the authored
|
|
178
|
+
journey immediately."""
|
|
179
|
+
if not callable(self._on_step):
|
|
180
|
+
return
|
|
181
|
+
if callable(self._catch):
|
|
182
|
+
kind = self._kind.currentText()
|
|
183
|
+
self._step_btn.setEnabled(False)
|
|
184
|
+
self._step_btn.setText(" freezing a trap…")
|
|
185
|
+
import threading
|
|
186
|
+
|
|
187
|
+
def work():
|
|
188
|
+
try:
|
|
189
|
+
fr = self._catch(kind)
|
|
190
|
+
except Exception:
|
|
191
|
+
fr = None
|
|
192
|
+
if not self._closed:
|
|
193
|
+
self.caught.emit(fr)
|
|
194
|
+
threading.Thread(target=work, daemon=True).start()
|
|
195
|
+
else:
|
|
196
|
+
self._on_step(None)
|
|
197
|
+
|
|
198
|
+
def _on_caught(self, frame) -> None:
|
|
199
|
+
self._step_btn.setEnabled(True)
|
|
200
|
+
self._step_btn.setText(" Step a trap ▸")
|
|
201
|
+
if not self._closed and callable(self._on_step):
|
|
202
|
+
self._on_step(frame)
|
|
203
|
+
|
|
204
|
+
def _fetch(self) -> None:
|
|
205
|
+
if self._busy or self._closed:
|
|
206
|
+
return
|
|
207
|
+
self._busy = True
|
|
208
|
+
import threading
|
|
209
|
+
|
|
210
|
+
def work():
|
|
211
|
+
try:
|
|
212
|
+
txt = self._src()
|
|
213
|
+
except Exception:
|
|
214
|
+
txt = ""
|
|
215
|
+
atxt = ""
|
|
216
|
+
if callable(self._alarm_src):
|
|
217
|
+
try:
|
|
218
|
+
atxt = self._alarm_src() or ""
|
|
219
|
+
except Exception:
|
|
220
|
+
atxt = ""
|
|
221
|
+
if not self._closed: # don't signal a dialog that's being torn down
|
|
222
|
+
self.traps_ready.emit(txt or "")
|
|
223
|
+
if callable(self._alarm_src):
|
|
224
|
+
self.alarms_ready.emit(atxt)
|
|
225
|
+
threading.Thread(target=work, daemon=True).start()
|
|
226
|
+
|
|
227
|
+
def _apply_alarms(self, txt) -> None:
|
|
228
|
+
if self._closed:
|
|
229
|
+
return
|
|
230
|
+
alarms = parse_alarms(txt)
|
|
231
|
+
if not alarms:
|
|
232
|
+
self._alarms.setText("⏰ no alarm set — a process calls sigalarm(interval, handler) to "
|
|
233
|
+
"run a periodic user-level handler (the sigalarm lab).")
|
|
234
|
+
return
|
|
235
|
+
lines = []
|
|
236
|
+
for pid in sorted(alarms):
|
|
237
|
+
a = alarms[pid]
|
|
238
|
+
state = " · handler RUNNING" if a.on else ""
|
|
239
|
+
lines.append(f"⏰ pid {pid} · every {a.interval} ticks · fires in {a.remaining} · "
|
|
240
|
+
f"handler {a.handler}{state}")
|
|
241
|
+
self._alarms.setText("\n".join(lines))
|
|
242
|
+
|
|
243
|
+
def _apply(self, txt) -> None:
|
|
244
|
+
self._busy = False
|
|
245
|
+
if self._closed:
|
|
246
|
+
return
|
|
247
|
+
import time
|
|
248
|
+
counts = parse_trapcounts(txt)
|
|
249
|
+
if counts:
|
|
250
|
+
self._rate.add(time.monotonic(), counts)
|
|
251
|
+
rates = self._rate.rates()
|
|
252
|
+
# all six kinds, biggest first, so the mix is always fully visible (zeros included)
|
|
253
|
+
rows = sorted(((k, trap_kind_name(k), rates.get(k, 0)) for k in TRAP_KINDS),
|
|
254
|
+
key=lambda r: -r[2])
|
|
255
|
+
self._bars.set_data(rows)
|
|
256
|
+
evs = parse_traptrace(txt)
|
|
257
|
+
if evs:
|
|
258
|
+
lines = [f"{e.pid:>3} {trap_kind_name(e.kind):<9} {e.epc}"
|
|
259
|
+
+ (f" {e.tval}" if e.tval not in ("0x0", "0x0000000000000000") else "")
|
|
260
|
+
for e in evs]
|
|
261
|
+
sb = self._feed.verticalScrollBar()
|
|
262
|
+
at_bottom = sb.value() >= sb.maximum() - 4
|
|
263
|
+
self._feed.setPlainText("\n".join(lines))
|
|
264
|
+
if at_bottom:
|
|
265
|
+
sb.setValue(sb.maximum())
|
|
266
|
+
|
|
267
|
+
def closeEvent(self, e) -> None: # noqa: N802
|
|
268
|
+
self._closed = True
|
|
269
|
+
self._poll.stop()
|
|
270
|
+
super().closeEvent(e)
|
gini/ui/worker_host.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Running slow hardware work off the GUI thread, safely.
|
|
2
|
+
|
|
3
|
+
Extracted from board_dialog so that every hardware dialog shares ONE copy. The rules
|
|
4
|
+
below are not stylistic — each one is a bug that aborted the whole process when it was
|
|
5
|
+
found by running the setup dialog headless, and re-deriving them per dialog is how they
|
|
6
|
+
come back:
|
|
7
|
+
|
|
8
|
+
1. **Hold a Python reference to the worker AND the thread.** PySide does not keep a
|
|
9
|
+
worker alive merely because a signal is connected to it, so a worker passed in as a
|
|
10
|
+
temporary is collected the moment the call returns; the thread then sits in its event
|
|
11
|
+
loop forever and nothing is ever emitted.
|
|
12
|
+
2. **Connect only to BOUND METHODS of the dialog, never to a closure.** A closure has no
|
|
13
|
+
thread affinity, so Qt chooses a direct connection and the "off-thread" handler runs
|
|
14
|
+
ON the worker thread, touching widgets from outside the GUI thread.
|
|
15
|
+
3. **Never parent the QThread to the dialog.** Serial work is slow — a board reboots when
|
|
16
|
+
its port is opened — so the dialog can easily be closed mid-run; destroying a running
|
|
17
|
+
QThread makes Qt abort. Unparented, the module registry holds it until it finishes.
|
|
18
|
+
|
|
19
|
+
A host must provide `_alive: bool` and a `_worker_failed(str)` slot.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
25
|
+
from PySide6.QtCore import QObject, QThread
|
|
26
|
+
|
|
27
|
+
# Workers and their threads outlive the dialog that started them, so they need an owner
|
|
28
|
+
# that is not the dialog. Entries retire themselves when the thread ends.
|
|
29
|
+
_LIVE_THREADS: set = set() # {(QThread, worker)}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def drain(timeout_ms: int = 3000) -> int:
|
|
33
|
+
"""Stop and reap every live worker thread. Returns how many were still running.
|
|
34
|
+
|
|
35
|
+
`_detach` makes a running worker harmless to the DIALOG, but the thread itself is
|
|
36
|
+
still owned by the registry and is only retired when its `finished` signal is
|
|
37
|
+
delivered — which needs an event loop to be turning. At application shutdown (or at
|
|
38
|
+
the end of a test run) there may be none left, and Python then garbage-collects a
|
|
39
|
+
QThread that is still running, which is precisely the "Destroyed while thread is
|
|
40
|
+
still running" abort that this module exists to prevent. So ask them to stop, and
|
|
41
|
+
wait.
|
|
42
|
+
"""
|
|
43
|
+
stragglers = 0
|
|
44
|
+
for thread, _worker in list(_LIVE_THREADS):
|
|
45
|
+
try:
|
|
46
|
+
if thread.isRunning():
|
|
47
|
+
stragglers += 1
|
|
48
|
+
thread.quit()
|
|
49
|
+
thread.wait(timeout_ms)
|
|
50
|
+
except RuntimeError:
|
|
51
|
+
pass # already deleted by Qt; nothing to reap
|
|
52
|
+
_LIVE_THREADS.discard((thread, _worker))
|
|
53
|
+
return stragglers
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WorkerHost:
|
|
57
|
+
"""Mixin: `self._run(worker, on_done)` and `self._detach()`."""
|
|
58
|
+
|
|
59
|
+
def _run(self, worker: QObject, on_done) -> None:
|
|
60
|
+
thread = QThread()
|
|
61
|
+
worker.moveToThread(thread)
|
|
62
|
+
thread.started.connect(worker.run)
|
|
63
|
+
entry = (thread, worker) # rule 1
|
|
64
|
+
_LIVE_THREADS.add(entry)
|
|
65
|
+
self._worker = worker
|
|
66
|
+
|
|
67
|
+
worker.done.connect(on_done) # rule 2: a bound method of the dialog
|
|
68
|
+
worker.done.connect(thread.quit)
|
|
69
|
+
self._connections = [(worker.done, on_done)]
|
|
70
|
+
if hasattr(worker, "failed"):
|
|
71
|
+
worker.failed.connect(self._worker_failed)
|
|
72
|
+
worker.failed.connect(thread.quit)
|
|
73
|
+
self._connections.append((worker.failed, self._worker_failed))
|
|
74
|
+
if hasattr(worker, "progress") and hasattr(self, "_worker_progress"):
|
|
75
|
+
worker.progress.connect(self._worker_progress)
|
|
76
|
+
self._connections.append((worker.progress, self._worker_progress))
|
|
77
|
+
thread.finished.connect(worker.deleteLater)
|
|
78
|
+
thread.finished.connect(lambda: _LIVE_THREADS.discard(entry))
|
|
79
|
+
thread.finished.connect(thread.deleteLater)
|
|
80
|
+
self._thread = thread # rule 3: unparented
|
|
81
|
+
thread.start()
|
|
82
|
+
|
|
83
|
+
def _detach(self) -> None:
|
|
84
|
+
"""Cut every path from a running worker back into this dialog.
|
|
85
|
+
|
|
86
|
+
An `_alive` flag alone is not enough: if the dialog is destroyed while a worker is
|
|
87
|
+
still going, the queued call lands on a freed C++ object and takes the process
|
|
88
|
+
with it. Only OUR handlers are disconnected — `thread.quit` stays connected, or
|
|
89
|
+
the thread would never stop.
|
|
90
|
+
"""
|
|
91
|
+
self._alive = False
|
|
92
|
+
for signal, slot in getattr(self, "_connections", []):
|
|
93
|
+
# A worker that already finished has dropped its connections, and PySide
|
|
94
|
+
# reports that as a warning rather than an exception. Closing a dialog after
|
|
95
|
+
# a successful run is the common case, so this must be silent.
|
|
96
|
+
with warnings.catch_warnings():
|
|
97
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
98
|
+
try:
|
|
99
|
+
signal.disconnect(slot)
|
|
100
|
+
except (RuntimeError, TypeError):
|
|
101
|
+
pass
|
|
102
|
+
self._connections = []
|
gini/ui/zoo_lab.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Zoo Lab — a historical OS from the OS Zoo, running under emulation with its screen embedded.
|
|
2
|
+
|
|
3
|
+
Open it by double-clicking an OS Zoo element (FreeDOS, Plan 9, ReactOS, or a bring-your-own
|
|
4
|
+
classic OS) once the topology is running. The container boots the guest under QEMU with a VNC
|
|
5
|
+
framebuffer and serves it as a web page (noVNC); this dialog embeds that page in a
|
|
6
|
+
`QWebEngineView`, so the OS runs right inside gBuilder — mouse, keyboard, boot and all.
|
|
7
|
+
|
|
8
|
+
The embedded page is the full noVNC client (`vnc.html`), which already carries its own toolbar
|
|
9
|
+
for power/keys (Ctrl-Alt-Del), clipboard and fullscreen; this wrapper just frames it and adds
|
|
10
|
+
Reload / Open-in-browser. Importing this module requires QtWebEngine — the caller catches an
|
|
11
|
+
ImportError and falls back to opening the same URL in the system browser, so the feature works
|
|
12
|
+
with no extra dependency.
|
|
13
|
+
|
|
14
|
+
Sibling of the Machine Lab (xv6): there you inspect an OS's internals; here you *use* a whole
|
|
15
|
+
historical OS live.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import QUrl
|
|
20
|
+
from PySide6.QtGui import QDesktopServices
|
|
21
|
+
from PySide6.QtWebEngineWidgets import QWebEngineView # ships in PySide6-Addons (or full PySide6)
|
|
22
|
+
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
|
23
|
+
|
|
24
|
+
from .theme import ThemeManager, icons
|
|
25
|
+
from .theme.manager import scale_css as _scss
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ZooLab(QDialog):
|
|
29
|
+
"""Embedded screen for one OS Zoo guest (noVNC in a QWebEngineView) + a small control strip."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, parent, theme: ThemeManager, device, url: str) -> None:
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
self.theme = theme
|
|
34
|
+
self.device = device
|
|
35
|
+
self.url = url
|
|
36
|
+
|
|
37
|
+
t = theme.theme
|
|
38
|
+
self.setWindowTitle(f"Zoo Lab — {device.name}")
|
|
39
|
+
self.resize(1024, 820) # 1024x768 guest + room for the header/controls
|
|
40
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
41
|
+
|
|
42
|
+
root = QVBoxLayout(self)
|
|
43
|
+
self._build_header(root)
|
|
44
|
+
self._view = QWebEngineView(self)
|
|
45
|
+
self._view.setUrl(QUrl(url))
|
|
46
|
+
root.addWidget(self._view, 1)
|
|
47
|
+
|
|
48
|
+
# -- header / controls --------------------------------------------------- #
|
|
49
|
+
def _build_header(self, root) -> None:
|
|
50
|
+
t = self.theme.theme
|
|
51
|
+
head = QHBoxLayout()
|
|
52
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("host", t.accent_for("orange"), 24))
|
|
53
|
+
title = QLabel(f" {self.device.name} — {self._label()}")
|
|
54
|
+
title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
|
|
55
|
+
head.addWidget(ic); head.addWidget(title); head.addStretch(1)
|
|
56
|
+
|
|
57
|
+
tk = self.device.type_key
|
|
58
|
+
try:
|
|
59
|
+
from ..domain import os_zoo
|
|
60
|
+
chip = ("bring your own image" if tk == "oszoo_byo"
|
|
61
|
+
else "boots out of the box" if os_zoo.get(tk) else "over noVNC")
|
|
62
|
+
except Exception:
|
|
63
|
+
chip = "over noVNC"
|
|
64
|
+
tier = QLabel(chip)
|
|
65
|
+
tier.setStyleSheet(
|
|
66
|
+
f"color:{t.muted};background:{t.panel2};border:1px solid {t.line};"
|
|
67
|
+
"border-radius:9px;padding:2px 10px;font-size:11px;")
|
|
68
|
+
head.addWidget(tier)
|
|
69
|
+
|
|
70
|
+
reload_btn = QPushButton(" Reload")
|
|
71
|
+
reload_btn.setIcon(icons.icon("link", t.accent_for("blue"), 14))
|
|
72
|
+
reload_btn.setToolTip("Reconnect the embedded screen")
|
|
73
|
+
reload_btn.clicked.connect(lambda: self._view.setUrl(QUrl(self.url)))
|
|
74
|
+
reload_btn.setStyleSheet(self._btn_css())
|
|
75
|
+
head.addWidget(reload_btn)
|
|
76
|
+
|
|
77
|
+
browser = QPushButton(" Open in browser")
|
|
78
|
+
browser.setIcon(icons.icon("open", t.accent_for("green"), 14))
|
|
79
|
+
browser.setToolTip("Open this OS's screen in your web browser instead")
|
|
80
|
+
browser.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(self.url)))
|
|
81
|
+
browser.setStyleSheet(self._btn_css())
|
|
82
|
+
head.addWidget(browser)
|
|
83
|
+
root.addLayout(head)
|
|
84
|
+
|
|
85
|
+
def _label(self) -> str:
|
|
86
|
+
tk = self.device.type_key
|
|
87
|
+
try:
|
|
88
|
+
from ..domain import os_zoo
|
|
89
|
+
if tk != "oszoo_byo":
|
|
90
|
+
o = os_zoo.get(tk)
|
|
91
|
+
if o:
|
|
92
|
+
return o.label
|
|
93
|
+
elif tk == "oszoo_byo":
|
|
94
|
+
img = (self.device.properties or {}).get("Image", "")
|
|
95
|
+
return f"classic OS ({img})" if img else "classic OS (set an image)"
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
try: # generic (e.g. a Desktop machine): element label
|
|
99
|
+
from ..domain.devices import REGISTRY
|
|
100
|
+
dt = REGISTRY.get(tk)
|
|
101
|
+
if dt:
|
|
102
|
+
return dt.label
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return tk
|
|
106
|
+
|
|
107
|
+
def _btn_css(self) -> str:
|
|
108
|
+
t = self.theme.theme
|
|
109
|
+
return _scss(
|
|
110
|
+
f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
111
|
+
f"border-radius:8px;padding:4px 12px;font-size:12px;}}"
|
|
112
|
+
f"QPushButton:hover{{border-color:{t.accent};}}")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gini-toolkit
|
|
3
|
+
Version: 6.0.1.dev0
|
|
4
|
+
Summary: GINI gBuilder — next-generation topology builder for computer networks and cloud computing experimentation
|
|
5
|
+
Author: GINI Toolkit
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: PySide6-Essentials>=6.6
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Provides-Extra: agent
|
|
12
|
+
Requires-Dist: mcp>=1.0; extra == "agent"
|
|
13
|
+
Provides-Extra: graph
|
|
14
|
+
Requires-Dist: matplotlib>=3.7; extra == "graph"
|
|
15
|
+
Requires-Dist: numpy>=1.24; extra == "graph"
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-qt>=4; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# GINI gBuilder — next generation
|
|
23
|
+
|
|
24
|
+
A ground-up rewrite of the GINI gBuilder frontend on **PySide6 (Qt 6) / Python 3**, expanding
|
|
25
|
+
GINI from a computer-networks lab into an **ultimate experimentation toolkit** spanning both
|
|
26
|
+
**computer networks** and **cloud computing**, with a first-class **AI agent layer** so agents can
|
|
27
|
+
build, inspect, and explain topologies for students.
|
|
28
|
+
|
|
29
|
+
## What's here
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
frontend-ng/
|
|
33
|
+
├── pyproject.toml
|
|
34
|
+
├── src/gini/
|
|
35
|
+
│ ├── __main__.py launch the app (python -m gini)
|
|
36
|
+
│ ├── domain/ pure-Python model — NO Qt
|
|
37
|
+
│ │ ├── devices.py device/element registry (networking + cloud)
|
|
38
|
+
│ │ └── topology.py Topology, DeviceInstance, Link
|
|
39
|
+
│ ├── app/
|
|
40
|
+
│ │ └── context.py AppContext + typed event bus (replaces global singletons)
|
|
41
|
+
│ ├── ui/
|
|
42
|
+
│ │ ├── main_window.py shell: toolbar, palette, canvas, inspector, console, assistant
|
|
43
|
+
│ │ ├── canvas.py QGraphicsView scene + node/edge items
|
|
44
|
+
│ │ ├── palette.py searchable, categorized device palette
|
|
45
|
+
│ │ ├── inspector.py tabbed property inspector
|
|
46
|
+
│ │ ├── assistant.py in-app "Ask GINI" AI assistant panel
|
|
47
|
+
│ │ └── theme/
|
|
48
|
+
│ │ ├── tokens.py themeable design tokens (dark / light / brand)
|
|
49
|
+
│ │ ├── manager.py token → QSS generator + ThemeManager
|
|
50
|
+
│ │ └── icons.py license-safe flat SVG icon set, recolored per category
|
|
51
|
+
│ └── agent/
|
|
52
|
+
│ ├── api.py programmatic GiniAPI: build / inspect / explain / run
|
|
53
|
+
│ └── mcp_server.py MCP server exposing GiniAPI to external agents
|
|
54
|
+
└── tests/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Run it
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd frontend-ng
|
|
61
|
+
pip install -e .
|
|
62
|
+
gbuilder # or: python -m gini
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Headless smoke test (no display needed):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
QT_QPA_PLATFORM=offscreen python -m gini --selftest
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Status
|
|
72
|
+
|
|
73
|
+
Early scaffold (Phase 2 of the evolution plan). The UI shell, themeable design system, icon
|
|
74
|
+
system, the expanded device taxonomy, and the agent API/MCP scaffold are in place and runnable.
|
|
75
|
+
The legacy `.gsav`/compiler/protocol services are being ported behind `services/` next.
|
|
76
|
+
|
|
77
|
+
See `../GINI Project/GINI_Frontend_Evolution_Plan.md` for the full roadmap.
|