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/game_catalog.py
ADDED
|
@@ -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])
|