gelang 0.1.0

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 (96) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/bin/ge.js +112 -0
  5. package/package.json +62 -0
  6. package/python/pyeffic/__init__.py +9 -0
  7. package/python/pyeffic/__main__.py +6 -0
  8. package/python/pyeffic/analyzer.py +464 -0
  9. package/python/pyeffic/apisurface.py +238 -0
  10. package/python/pyeffic/autoselect.py +327 -0
  11. package/python/pyeffic/backends.py +87 -0
  12. package/python/pyeffic/bench.py +233 -0
  13. package/python/pyeffic/cli.py +184 -0
  14. package/python/pyeffic/compiler.py +421 -0
  15. package/python/pyeffic/config.py +383 -0
  16. package/python/pyeffic/dartgen.py +441 -0
  17. package/python/pyeffic/deploy.py +586 -0
  18. package/python/pyeffic/diagnostics.py +194 -0
  19. package/python/pyeffic/difftest.py +424 -0
  20. package/python/pyeffic/downloader.py +307 -0
  21. package/python/pyeffic/emitters/__init__.py +11 -0
  22. package/python/pyeffic/emitters/base.py +2359 -0
  23. package/python/pyeffic/emitters/cpp.py +266 -0
  24. package/python/pyeffic/emitters/csharp.py +342 -0
  25. package/python/pyeffic/emitters/dart.py +349 -0
  26. package/python/pyeffic/emitters/go.py +388 -0
  27. package/python/pyeffic/emitters/kotlin.py +314 -0
  28. package/python/pyeffic/emitters/rust.py +314 -0
  29. package/python/pyeffic/emitters/zig.py +411 -0
  30. package/python/pyeffic/ffi.py +49 -0
  31. package/python/pyeffic/frontends/__init__.py +94 -0
  32. package/python/pyeffic/frontends/hybrid.py +709 -0
  33. package/python/pyeffic/frontends/typescript.py +965 -0
  34. package/python/pyeffic/ge_cli.py +1148 -0
  35. package/python/pyeffic/golden.py +348 -0
  36. package/python/pyeffic/idents.py +206 -0
  37. package/python/pyeffic/modules.py +220 -0
  38. package/python/pyeffic/packer.py +222 -0
  39. package/python/pyeffic/pipeline.py +797 -0
  40. package/python/pyeffic/reactgen.py +966 -0
  41. package/python/pyeffic/researcher.py +177 -0
  42. package/python/pyeffic/scaffold.py +397 -0
  43. package/python/pyeffic/stdlib.py +246 -0
  44. package/python/pyeffic/styling.py +220 -0
  45. package/python/pyeffic/templates/desktop_gui/README.md +106 -0
  46. package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
  47. package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
  48. package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
  49. package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
  50. package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
  51. package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
  52. package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
  53. package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
  54. package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
  55. package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
  56. package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
  57. package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
  58. package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
  59. package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
  60. package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
  61. package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
  62. package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
  63. package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
  64. package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
  65. package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
  66. package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
  67. package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
  68. package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
  69. package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
  70. package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
  71. package/python/pyeffic/templates/web_react/README.md +115 -0
  72. package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
  73. package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
  74. package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
  75. package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
  76. package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
  77. package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
  78. package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
  79. package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
  80. package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
  81. package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
  82. package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
  83. package/python/pyeffic/templates/web_react/ge.toml +23 -0
  84. package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
  85. package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
  86. package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
  87. package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
  88. package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
  89. package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
  90. package/python/pyeffic/ts2py.py +657 -0
  91. package/python/pyeffic/typecheck.py +232 -0
  92. package/python/pyeffic/ui.py +154 -0
  93. package/python/pyeffic/ui_dsl.py +618 -0
  94. package/python/pyeffic/widgets.py +87 -0
  95. package/scripts/README.md +42 -0
  96. package/scripts/check-toolchains.py +85 -0
@@ -0,0 +1,441 @@
1
+ """Generate Dart: FFI bindings + Flutter main.dart + pubspec.yaml.
2
+
3
+ Inputs:
4
+ - ffi_units: FuncUnits that are C-ABI exported (scalar signatures)
5
+ - tree: widget tree from ui.parse_ui
6
+ - state_fields: set of state field names
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .analyzer import FuncUnit
11
+ from .ffi import DART_FFI_TYPE, DART_NATIVE_TYPE, dart_name
12
+ from .ui import collect_state
13
+ from .styling import (style_to_flutter_text, style_to_flutter_padding,
14
+ style_to_flutter_box, style_to_flutter_main_align,
15
+ parse_style)
16
+
17
+ _STYLE = {
18
+ "headline": "TextStyle(fontSize: 28, fontWeight: FontWeight.bold)",
19
+ "title": "TextStyle(fontSize: 20, fontWeight: FontWeight.bold)",
20
+ "subtitle": "TextStyle(fontSize: 16, color: Colors.grey[700])",
21
+ "body": "TextStyle(fontSize: 16)",
22
+ }
23
+ _ALIGN = {"center": "center", "start": "start", "end": "end",
24
+ "spaceBetween": "spaceBetween", "spaceEvenly": "spaceEvenly",
25
+ "spaceAround": "spaceAround"}
26
+
27
+
28
+ def _ret_type(units: list[FuncUnit], name: str) -> str:
29
+ for u in units:
30
+ if u.name == name:
31
+ return u.ret_type
32
+ return "float"
33
+
34
+
35
+ def generate_bindings(ffi_units: list[FuncUnit], lib_basename: str,
36
+ func_backends: dict[str, str] | None = None,
37
+ lib_names: dict[str, str] | None = None) -> str:
38
+ """Generate Dart FFI bindings.
39
+
40
+ func_backends: maps function name -> backend ('rust' or 'cpp').
41
+ If None or empty, all functions use the single lib_basename.
42
+ lib_names: maps backend -> shared lib basename for that group.
43
+ If None or empty, all functions load from lib_basename.
44
+ """
45
+ lines = [
46
+ "// AUTO-GENERATED by pyeffic. Do not edit by hand.",
47
+ "import 'dart:ffi';",
48
+ "import 'dart:io';",
49
+ "",
50
+ ]
51
+ # typedefs + lookups
52
+ for u in ffi_units:
53
+ dn = dart_name(u.name)
54
+ native_params = ", ".join(DART_FFI_TYPE[t] for _, t in u.params)
55
+ dart_params = ", ".join(DART_NATIVE_TYPE[t] for _, t in u.params)
56
+ ret_n = DART_FFI_TYPE[u.ret_type]
57
+ ret_d = DART_NATIVE_TYPE[u.ret_type]
58
+ lines.append(f"typedef {dn}Native = {ret_n} Function({native_params});")
59
+ lines.append(f"typedef {dn}Dart = {ret_d} Function({dart_params});")
60
+ lines.append("")
61
+ lines.append("class NativeLib {")
62
+
63
+ # determine if we need multiple DLLs
64
+ backends_used = set()
65
+ if func_backends:
66
+ for u in ffi_units:
67
+ b = func_backends.get(u.name)
68
+ if b:
69
+ backends_used.add(b)
70
+ multi = len(backends_used) > 1
71
+
72
+ if multi:
73
+ # multi-backend: one DynamicLibrary per backend
74
+ for b in sorted(backends_used):
75
+ lines.append(f" late final DynamicLibrary _lib_{b};")
76
+ else:
77
+ lines.append(" late final DynamicLibrary _lib;")
78
+
79
+ for u in ffi_units:
80
+ lines.append(f" late final {dart_name(u.name)}Dart {dart_name(u.name)};")
81
+ lines.append("")
82
+ lines.append(" NativeLib() {")
83
+
84
+ if multi:
85
+ for b in sorted(backends_used):
86
+ lname = lib_names.get(b, lib_basename) if lib_names else lib_basename
87
+ lines.append(f" final name_{b} = Platform.isWindows")
88
+ lines.append(f" ? '{lname}.dll'")
89
+ lines.append(f" : Platform.isLinux")
90
+ lines.append(f" ? 'lib{lname}.so'")
91
+ lines.append(f" : 'lib{lname}.dylib';")
92
+ lines.append(f" _lib_{b} = DynamicLibrary.open(name_{b});")
93
+ else:
94
+ lines.append(" final name = Platform.isWindows")
95
+ lines.append(f" ? '{lib_basename}.dll'")
96
+ lines.append(" : Platform.isLinux")
97
+ lines.append(f" ? 'lib{lib_basename}.so'")
98
+ lines.append(f" : 'lib{lib_basename}.dylib';")
99
+ lines.append(" _lib = DynamicLibrary.open(name);")
100
+
101
+ for u in ffi_units:
102
+ dn = dart_name(u.name)
103
+ b = func_backends.get(u.name) if func_backends else None
104
+ if multi and b:
105
+ lines.append(f" {dn} = _lib_{b}.lookupFunction<{dn}Native, {dn}Dart>('{u.name}');")
106
+ else:
107
+ lines.append(f" {dn} = _lib.lookupFunction<{dn}Native, {dn}Dart>('{u.name}');")
108
+ lines.append(" }")
109
+ lines.append("}")
110
+ return "\n".join(lines) + "\n"
111
+
112
+
113
+ def _widget_dart(node, state_fields: set[str], indent: int, _id_counter: list | None = None) -> str:
114
+ pad = " " * indent
115
+ if _id_counter is None:
116
+ _id_counter = [0]
117
+ if not isinstance(node, dict):
118
+ _id_counter[0] += 1
119
+ ref = f"ge:Text:{_id_counter[0]}"
120
+ return f"{pad}GeTagged(ref: {ref!r}, child: Text({node!r}))"
121
+ k = node.get("kind")
122
+ _id_counter[0] += 1
123
+ ref = f"ge:{k}:{_id_counter[0]}"
124
+ if k == "Text":
125
+ sd = node.get("style_dict", {})
126
+ if node.get("state"):
127
+ ts = style_to_flutter_text(sd) if sd else _STYLE.get("title", _STYLE["body"])
128
+ inner = f"Text(_{node['state']}, style: {ts})"
129
+ else:
130
+ ts = style_to_flutter_text(sd) if sd else _STYLE.get(node.get("style", "body"), _STYLE["body"])
131
+ inner = f"Text({_dart_str(node['text'])}, style: {ts})"
132
+ return f"{pad}GeTagged(ref: {ref!r}, child: {inner})"
133
+ if k in ("Column", "Row"):
134
+ children = node.get("children", [])
135
+ sd = node.get("style_dict", {})
136
+ main_axis = sd.get("align", node.get("align", "center")) if sd else node.get("align", "center")
137
+ main_expr = style_to_flutter_main_align({"align": main_axis})
138
+ cross = "start" if k == "Row" else "center"
139
+ inner = ",\n".join(_widget_dart(c, state_fields, indent + 3, _id_counter) for c in children)
140
+ return (f"{pad}GeTagged(ref: {ref!r}, child: {k}(\n"
141
+ f"{pad} mainAxisAlignment: {main_expr},\n"
142
+ f"{pad} crossAxisAlignment: CrossAxisAlignment.{cross},\n"
143
+ f"{pad} children: [\n{inner}\n{pad} ],\n{pad}))")
144
+ if k == "Container":
145
+ child = node.get("child")
146
+ sd = node.get("style_dict", {})
147
+ color = node.get("color", "")
148
+ if "padding" in sd:
149
+ pad_expr = style_to_flutter_padding(sd)
150
+ else:
151
+ pad_expr = f"EdgeInsets.all({node.get('padding', 8)})"
152
+ box = style_to_flutter_box(sd)
153
+ if not box and color:
154
+ box = f"BoxDecoration(color: Colors.{color})"
155
+ box_part = f"decoration: {box},\n{pad} " if box else ""
156
+ child_s = _widget_dart(child, state_fields, indent + 3, _id_counter) if child else f"{' '*(indent+3)}const SizedBox.shrink()"
157
+ return (f"{pad}GeTagged(ref: {ref!r}, child: Container(\n"
158
+ f"{pad} padding: const {pad_expr},\n"
159
+ f"{pad} {box_part}child: {child_s.strip()},\n{pad}))")
160
+ if k == "Button":
161
+ label = node.get("label", "")
162
+ action = node.get("action")
163
+ handler = "null"
164
+ if isinstance(action, list) and action and action[0].get("call"):
165
+ handler = action[0].get("_handler", f"_{action[0]['call']}")
166
+ return (f"{pad}GeTagged(ref: {ref!r}, child: ElevatedButton(\n"
167
+ f"{pad} onPressed: {handler},\n"
168
+ f"{pad} child: Text({_dart_str(label)}),\n{pad}))")
169
+ if k == "SizedBox":
170
+ h = node.get("height", 0)
171
+ w = node.get("width", 0)
172
+ if h and w:
173
+ return f"{pad}GeTagged(ref: {ref!r}, child: SizedBox(height: {h}, width: {w}))"
174
+ if h:
175
+ return f"{pad}GeTagged(ref: {ref!r}, child: SizedBox(height: {h}))"
176
+ if w:
177
+ return f"{pad}GeTagged(ref: {ref!r}, child: SizedBox(width: {w}))"
178
+ return f"{pad}GeTagged(ref: {ref!r}, child: const SizedBox.shrink())"
179
+ if k == "Divider":
180
+ return f"{pad}GeTagged(ref: {ref!r}, child: const Divider(thickness: 1))"
181
+ if k == "Expanded":
182
+ child = node.get("child")
183
+ child_s = _widget_dart(child, state_fields, indent + 1, _id_counter) if child else f"{pad}const SizedBox.shrink()"
184
+ flex = node.get("flex", 1)
185
+ return f"{pad}GeTagged(ref: {ref!r}, child: Expanded(flex: {flex}, child: {child_s.strip()}))"
186
+ return f"{pad}GeTagged(ref: {ref!r}, child: Text('unsupported: {k}'))"
187
+
188
+
189
+ def _handler_method(action_name: str, ffi_units: list[FuncUnit]) -> str:
190
+ """Generate a Dart handler method for an Action."""
191
+ # find the unit with this name
192
+ unit = next((u for u in ffi_units if u.name == action_name), None)
193
+ if unit is None:
194
+ return f" void _{action_name}() {{ /* FFI fn '{action_name}' not exported */ }}\n"
195
+ dn = dart_name(unit.name)
196
+ args = ", ".join(str(a) for a in []) # filled below
197
+ # We need the action's args; this method is built per-button in generate_main
198
+ return "" # placeholder; real handlers built in generate_main
199
+
200
+
201
+ def generate_main(tree: dict, ffi_units: list[FuncUnit], app_title: str,
202
+ dart_source: str = "") -> str:
203
+ state_fields = collect_state(tree)
204
+ # discover buttons (each has a list of actions for multi-update)
205
+ buttons = []
206
+
207
+ def walk(n):
208
+ if isinstance(n, dict):
209
+ if n.get("kind") == "Button" and isinstance(n.get("action"), list):
210
+ buttons.append(n)
211
+ for key in ("children", "child"):
212
+ v = n.get(key)
213
+ if isinstance(v, list):
214
+ for c in v:
215
+ walk(c)
216
+ elif isinstance(v, dict):
217
+ walk(v)
218
+
219
+ walk(tree)
220
+
221
+ # assign unique handler names per button (based on first action's call)
222
+ seen: dict[str, int] = {}
223
+ for btn in buttons:
224
+ acts = btn["action"]
225
+ if not acts:
226
+ continue
227
+ base = acts[0].get("call", "handler")
228
+ if base not in seen:
229
+ seen[base] = 1
230
+ hname = f"_{base}"
231
+ else:
232
+ seen[base] += 1
233
+ hname = f"_{base}_{seen[base]}"
234
+ # store handler name on the first action (read by _widget_dart)
235
+ acts[0]["_handler"] = hname
236
+ btn["_handler_name"] = hname
237
+
238
+ body_widget = _widget_dart(tree, state_fields, 6, [0])
239
+
240
+ # state fields
241
+ state_decls = "\n".join(f" String _{f} = '';" for f in sorted(state_fields)) or " // no state"
242
+
243
+ # handlers — one per button, calling all its actions in a single setState
244
+ handlers = []
245
+ for btn in buttons:
246
+ acts = btn.get("action", [])
247
+ if not acts:
248
+ continue
249
+ hname = btn.get("_handler_name", f"_{acts[0].get('call', 'handler')}")
250
+ body_lines = []
251
+ setstate_lines = []
252
+ for act in acts:
253
+ call = act.get("call", "")
254
+ unit = next((u for u in ffi_units if u.name == call), None)
255
+ dn = dart_name(call)
256
+ args = ", ".join(_dart_arg(a) for a in act.get("args", []))
257
+ update = act.get("update", "")
258
+ if unit is None:
259
+ body_lines.append(f" // FFI fn '{call}' not exported")
260
+ continue
261
+ ret = unit.ret_type
262
+ if ret == "float":
263
+ body_lines.append(f" final r_{update} = _lib.{dn}({args});")
264
+ setstate_lines.append(f" _{update} = r_{update}.toStringAsFixed(2);")
265
+ elif ret == "int":
266
+ body_lines.append(f" final r_{update} = _lib.{dn}({args});")
267
+ setstate_lines.append(f" _{update} = r_{update}.toString();")
268
+ elif ret == "bool":
269
+ body_lines.append(f" final r_{update} = _lib.{dn}({args});")
270
+ setstate_lines.append(f" _{update} = r_{update} ? 'true' : 'false';")
271
+ else:
272
+ body_lines.append(f" _lib.{dn}({args});")
273
+ if setstate_lines:
274
+ body_lines.append(f" setState(() {{\n"
275
+ + "\n".join(setstate_lines) + "\n });")
276
+ else:
277
+ body_lines.append(" setState(() {});")
278
+ handlers.append(f" void {hname}() {{\n" + "\n".join(body_lines) + "\n }")
279
+ handlers_s = "\n\n".join(handlers) or " // no handlers"
280
+
281
+ return f"""// AUTO-GENERATED by pyeffic. Do not edit by hand.
282
+ import 'package:flutter/material.dart';
283
+ import 'package:flutter/services.dart';
284
+ import 'bindings.dart';
285
+ {dart_source}
286
+
287
+ void main() => runApp(const MyApp());
288
+
289
+ class MyApp extends StatelessWidget {{
290
+ const MyApp({{super.key}});
291
+
292
+ @override
293
+ Widget build(BuildContext context) {{
294
+ return MaterialApp(
295
+ title: {app_title!r},
296
+ home: const HomePage(),
297
+ );
298
+ }}
299
+ }}
300
+
301
+ class HomePage extends StatefulWidget {{
302
+ const HomePage({{super.key}});
303
+
304
+ @override
305
+ State<HomePage> createState() => _HomePageState();
306
+ }}
307
+
308
+ class _HomePageState extends State<HomePage> {{
309
+ final NativeLib _lib = NativeLib();
310
+
311
+ {state_decls}
312
+
313
+ {handlers_s}
314
+
315
+ @override
316
+ Widget build(BuildContext context) {{
317
+ return Scaffold(
318
+ appBar: AppBar(title: Text({app_title!r})),
319
+ body: SafeArea(
320
+ child: GeInspector(
321
+ child: SingleChildScrollView(
322
+ child: Padding(
323
+ padding: const EdgeInsets.all(16.0),
324
+ child: {body_widget.strip()},
325
+ ),
326
+ ),
327
+ ),
328
+ ),
329
+ floatingActionButton: const GeInspectorToggle(),
330
+ );
331
+ }}
332
+ }}
333
+
334
+ /// Tags a widget with a source reference for the GE inspector.
335
+ /// When inspector mode is enabled, tapping this widget copies its ref to clipboard.
336
+ class GeTagged extends StatelessWidget {{
337
+ final String ref;
338
+ final Widget child;
339
+ const GeTagged({{super.key, required this.ref, required this.child}});
340
+
341
+ @override
342
+ Widget build(BuildContext context) {{
343
+ if (GeInspectorState.isEnabled) {{
344
+ return GestureDetector(
345
+ onTap: () {{
346
+ Clipboard.setData(ClipboardData(text: ref));
347
+ ScaffoldMessenger.of(context).showSnackBar(
348
+ SnackBar(
349
+ content: Text('Copied: $ref'),
350
+ duration: const Duration(seconds: 2),
351
+ ),
352
+ );
353
+ }},
354
+ child: Container(
355
+ decoration: BoxDecoration(
356
+ border: Border.all(color: Colors.blue.withOpacity(0.3), width: 1),
357
+ ),
358
+ child: child,
359
+ ),
360
+ );
361
+ }}
362
+ return child;
363
+ }}
364
+ }}
365
+
366
+ /// Toggle button for inspector mode.
367
+ class GeInspectorToggle extends StatefulWidget {{
368
+ const GeInspectorToggle({{super.key}});
369
+ @override
370
+ State<GeInspectorToggle> createState() => _GeInspectorToggleState();
371
+ }}
372
+
373
+ class _GeInspectorToggleState extends State<GeInspectorToggle> {{
374
+ @override
375
+ Widget build(BuildContext context) {{
376
+ return FloatingActionButton(
377
+ mini: true,
378
+ backgroundColor: GeInspectorState.isEnabled ? Colors.blue : Colors.grey,
379
+ onPressed: () {{
380
+ setState(() {{
381
+ GeInspectorState.isEnabled = !GeInspectorState.isEnabled;
382
+ }});
383
+ }},
384
+ child: Icon(GeInspectorState.isEnabled ? Icons.bug_report : Icons.bug_report_outlined),
385
+ );
386
+ }}
387
+ }}
388
+
389
+ /// Global state for the GE inspector.
390
+ class GeInspectorState {{
391
+ static bool isEnabled = false;
392
+ }}
393
+
394
+ /// Wraps the app body with inspector overlay support.
395
+ class GeInspector extends StatelessWidget {{
396
+ final Widget child;
397
+ const GeInspector({{super.key, required this.child}});
398
+
399
+ @override
400
+ Widget build(BuildContext context) {{
401
+ return child;
402
+ }}
403
+ }}
404
+ """
405
+
406
+
407
+ def _dart_str(s) -> str:
408
+ """Produce a Dart string literal with $ escaped (no interpolation)."""
409
+ return repr(str(s)).replace("$", "\\$")
410
+
411
+
412
+ def _dart_arg(v) -> str:
413
+ if isinstance(v, bool):
414
+ return "true" if v else "false"
415
+ if isinstance(v, float):
416
+ return repr(v)
417
+ if isinstance(v, int):
418
+ return str(v)
419
+ return repr(str(v))
420
+
421
+
422
+ def generate_pubspec(app_name: str) -> str:
423
+ return f"""name: {app_name}
424
+ description: Generated by pyeffic (Python -> Flutter + native FFI).
425
+ publish_to: 'none'
426
+ version: 0.1.0
427
+
428
+ environment:
429
+ sdk: '>=3.0.0 <4.0.0'
430
+
431
+ dependencies:
432
+ flutter:
433
+ sdk: flutter
434
+
435
+ dev_dependencies:
436
+ flutter_test:
437
+ sdk: flutter
438
+
439
+ flutter:
440
+ uses-material-design: true
441
+ """