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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/ge.js +112 -0
- package/package.json +62 -0
- package/python/pyeffic/__init__.py +9 -0
- package/python/pyeffic/__main__.py +6 -0
- package/python/pyeffic/analyzer.py +464 -0
- package/python/pyeffic/apisurface.py +238 -0
- package/python/pyeffic/autoselect.py +327 -0
- package/python/pyeffic/backends.py +87 -0
- package/python/pyeffic/bench.py +233 -0
- package/python/pyeffic/cli.py +184 -0
- package/python/pyeffic/compiler.py +421 -0
- package/python/pyeffic/config.py +383 -0
- package/python/pyeffic/dartgen.py +441 -0
- package/python/pyeffic/deploy.py +586 -0
- package/python/pyeffic/diagnostics.py +194 -0
- package/python/pyeffic/difftest.py +424 -0
- package/python/pyeffic/downloader.py +307 -0
- package/python/pyeffic/emitters/__init__.py +11 -0
- package/python/pyeffic/emitters/base.py +2359 -0
- package/python/pyeffic/emitters/cpp.py +266 -0
- package/python/pyeffic/emitters/csharp.py +342 -0
- package/python/pyeffic/emitters/dart.py +349 -0
- package/python/pyeffic/emitters/go.py +388 -0
- package/python/pyeffic/emitters/kotlin.py +314 -0
- package/python/pyeffic/emitters/rust.py +314 -0
- package/python/pyeffic/emitters/zig.py +411 -0
- package/python/pyeffic/ffi.py +49 -0
- package/python/pyeffic/frontends/__init__.py +94 -0
- package/python/pyeffic/frontends/hybrid.py +709 -0
- package/python/pyeffic/frontends/typescript.py +965 -0
- package/python/pyeffic/ge_cli.py +1148 -0
- package/python/pyeffic/golden.py +348 -0
- package/python/pyeffic/idents.py +206 -0
- package/python/pyeffic/modules.py +220 -0
- package/python/pyeffic/packer.py +222 -0
- package/python/pyeffic/pipeline.py +797 -0
- package/python/pyeffic/reactgen.py +966 -0
- package/python/pyeffic/researcher.py +177 -0
- package/python/pyeffic/scaffold.py +397 -0
- package/python/pyeffic/stdlib.py +246 -0
- package/python/pyeffic/styling.py +220 -0
- package/python/pyeffic/templates/desktop_gui/README.md +106 -0
- package/python/pyeffic/templates/desktop_gui/app/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/core/add.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/factorial.ge.py +20 -0
- package/python/pyeffic/templates/desktop_gui/app/core/fibonacci.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/core/gcd.ge.py +19 -0
- package/python/pyeffic/templates/desktop_gui/app/core/is_prime.ge.py +24 -0
- package/python/pyeffic/templates/desktop_gui/app/core/multiply.ge.py +13 -0
- package/python/pyeffic/templates/desktop_gui/app/core/power.ge.py +25 -0
- package/python/pyeffic/templates/desktop_gui/app/main.ge.py +49 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/buffer.ge.py +26 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/limits.ge.py +47 -0
- package/python/pyeffic/templates/desktop_gui/app/memory/state.ge.py +44 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/layout.ge.py +64 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/render.ge.py +87 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/theme.ge.py +147 -0
- package/python/pyeffic/templates/desktop_gui/app/ui/widgets.ge.py +105 -0
- package/python/pyeffic/templates/desktop_gui/desktop/__init__.py +1 -0
- package/python/pyeffic/templates/desktop_gui/desktop/main.ge.py +258 -0
- package/python/pyeffic/templates/desktop_gui/ge.toml +16 -0
- package/python/pyeffic/templates/desktop_gui/tests/__init__.py +0 -0
- package/python/pyeffic/templates/desktop_gui/tests/ge_loader.py +76 -0
- package/python/pyeffic/templates/desktop_gui/tests/test_app.py +173 -0
- package/python/pyeffic/templates/web_react/README.md +115 -0
- package/python/pyeffic/templates/web_react/app/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/core/add.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/core/factorial.ge.py +16 -0
- package/python/pyeffic/templates/web_react/app/core/fibonacci.ge.py +21 -0
- package/python/pyeffic/templates/web_react/app/core/is_prime.ge.py +20 -0
- package/python/pyeffic/templates/web_react/app/core/multiply.ge.py +9 -0
- package/python/pyeffic/templates/web_react/app/main.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/app/memory/buffer.ge.py +25 -0
- package/python/pyeffic/templates/web_react/app/memory/limits.ge.py +51 -0
- package/python/pyeffic/templates/web_react/ge.toml +23 -0
- package/python/pyeffic/templates/web_react/tests/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/tests/ge_loader.py +68 -0
- package/python/pyeffic/templates/web_react/tests/test_app.py +105 -0
- package/python/pyeffic/templates/web_react/ui/main.ge.ui +33 -0
- package/python/pyeffic/templates/web_react/web/__init__.py +0 -0
- package/python/pyeffic/templates/web_react/web/server.ge.py +78 -0
- package/python/pyeffic/ts2py.py +657 -0
- package/python/pyeffic/typecheck.py +232 -0
- package/python/pyeffic/ui.py +154 -0
- package/python/pyeffic/ui_dsl.py +618 -0
- package/python/pyeffic/widgets.py +87 -0
- package/scripts/README.md +42 -0
- 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
|
+
"""
|