zagred 0.1.0__tar.gz

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.
zagred-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Arief Minardi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
zagred-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,467 @@
1
+ Metadata-Version: 2.4
2
+ Name: zagred
3
+ Version: 0.1.0
4
+ Summary: Desktop apps from Python backends and web frontends, built with pywebview and Nuitka
5
+ Author: Muhammad Arief Minardi
6
+ Author-email: Muhammad Arief Minardi <ariefminardi005@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Classifier: Topic :: Software Development :: User Interfaces
13
+ Classifier: Environment :: X11 Applications :: GTK
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: MacOS
17
+ Requires-Dist: pywebview>=6.0
18
+ Requires-Dist: nuitka>=4.2 ; extra == 'build'
19
+ Requires-Python: >=3.12
20
+ Provides-Extra: build
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Zagred
24
+
25
+ **Desktop apps with a Python backend and a web frontend.**
26
+
27
+ Your app is a normal Python package. Your UI is a folder next to it. Zagred puts
28
+ the two together in a native window (via [pywebview](https://pywebview.flowrl.com))
29
+ and compiles the result into a `.AppImage`, `.exe`, `.app` or `.dmg`
30
+ (via [Nuitka](https://nuitka.net)).
31
+
32
+ - **Python 3.12+**, dependencies managed by [uv](https://docs.astral.sh/uv/)
33
+ - No Rust. No Node.js unless you want a bundler.
34
+ - Frontend: plain HTML/JS, or React / Vue / Solid with Vite
35
+ - Bridge with typed errors, async commands, and Python→JS events
36
+
37
+ ---
38
+
39
+ ## Quick start
40
+
41
+ ```bash
42
+ # 1. install the CLI (not on PyPI yet — see "Local development")
43
+ uv tool install zagred
44
+
45
+ # 2. create a project — interactive: name, framework, JS or TS
46
+ zagred new
47
+
48
+ # 3. install everything
49
+ cd my-app
50
+ uv sync
51
+ (cd web && npm install) # only if you picked a bundler
52
+
53
+ # 4. run it
54
+ uv run zagred dev
55
+ ```
56
+
57
+ Skip the questions in scripts:
58
+
59
+ ```bash
60
+ zagred new my-app --template react --lang ts -y
61
+ ```
62
+
63
+ ### Linux: one extra step
64
+
65
+ uv's Python cannot import the distro's `python3-gi`, so build the venv from the
66
+ system interpreter **before** `uv sync`:
67
+
68
+ ```bash
69
+ sudo apt install python3-gi gir1.2-webkit2-4.1
70
+ uv venv --python /usr/bin/python3 --system-site-packages
71
+ uv sync
72
+ ```
73
+
74
+ Verify with `uv run zagred doctor` — it should report `renderer gtk`.
75
+
76
+ ### What you get
77
+
78
+ ```
79
+ my-app/
80
+ ├── zagred.toml # [app] identity, [ui] dirs, [build] options
81
+ ├── pyproject.toml # uv project, depends on zagred
82
+ ├── .python-version # pinned to 3.12
83
+ ├── web/ # your UI
84
+ │ ├── package.json # only when a bundler is used
85
+ │ └── src/
86
+ └── my_app/
87
+ └── app.py # App, @expose, lifecycle
88
+ ```
89
+
90
+ ---
91
+
92
+ ## The backend
93
+
94
+ Windows and lifecycle live in Python, not in JSON:
95
+
96
+ ```python
97
+ # my_app/app.py
98
+ import zagred
99
+
100
+ app = zagred.App(title="My App", size=(1024, 700))
101
+
102
+
103
+ @app.expose
104
+ def greet(name: str) -> str:
105
+ """Callable from the UI as zagred.call("greet", { name })."""
106
+ return f"Hello, {name}!"
107
+
108
+
109
+ @app.expose
110
+ async def scan(folder: str) -> list[str]:
111
+ ...
112
+
113
+
114
+ @app.expose
115
+ def pick_file() -> str | None:
116
+ files = app.create_file_dialog()
117
+ return files[0] if files else None
118
+ ```
119
+
120
+ Rules worth knowing:
121
+
122
+ - A function is registered under its `__name__`; exposing two functions with the
123
+ same name silently keeps the last one.
124
+ - Arguments are keyword arguments from JS. Wrong ones give `bad_arguments`.
125
+ - Every return value must be JSON-serialisable; that is checked before it leaves
126
+ Python, so you get an error instead of a silent blank window.
127
+ - `async def` works — it is awaited on a worker thread.
128
+
129
+ ## The frontend
130
+
131
+ ```js
132
+ const msg = await zagred.call("greet", { name: "world" });
133
+ ```
134
+
135
+ ```js
136
+ zagred.call("greet").catch((err) => {
137
+ console.error(err.code, err.message); // e.g. "bad_arguments"
138
+ });
139
+ ```
140
+
141
+ Events, Python → JS:
142
+
143
+ ```python
144
+ @app.expose
145
+ def start_job() -> None:
146
+ for done, total in work():
147
+ app.emit("job:progress", {"done": done, "total": total})
148
+ ```
149
+
150
+ ```js
151
+ const off = zagred.on("job:progress", (p) => console.log(p.done));
152
+ off(); // unsubscribe
153
+ ```
154
+
155
+ `zagred.ready` is a promise that resolves once the bridge is live. Top-level
156
+ script code should `await zagred.ready` (or call from inside an event handler)
157
+ because the bridge is injected around page-load time.
158
+
159
+ ---
160
+
161
+ ## CLI
162
+
163
+ | Command | What it does |
164
+ |---|---|
165
+ | `zagred new [name]` | Interactive scaffold. `--template`, `--lang`, `-y` for scripts |
166
+ | `zagred dev [--debug]` | Start the frontend dev server, wait for it, open the window |
167
+ | `zagred build [--target T] [--jobs N] [--debug]` | Build the UI, compile with Nuitka |
168
+ | `zagred doctor` | Check python, pywebview, renderer, nuitka, patchelf, appimagetool |
169
+
170
+ `zagred dev` cleans up after itself: closing the window kills the whole frontend
171
+ process group, so no orphaned `vite` holding your terminal.
172
+
173
+ ### Templates
174
+
175
+ | `--template` | `--lang js` | `--lang ts` | Bundler |
176
+ |---|---|---|---|
177
+ | `vanilla` *(default)* | plain files, **no npm** | Vite | optional |
178
+ | `react` | Vite | Vite | yes |
179
+ | `vue` | Vite | Vite | yes |
180
+ | `solid` | Vite | Vite | yes |
181
+
182
+ TypeScript projects ship `web/src/zagred.d.ts`:
183
+
184
+ ```ts
185
+ const msg = await zagred.call<string>("greet", { name });
186
+ ```
187
+
188
+ Bundler projects get `npm run dev -- --port 5173 --strictPort` in `zagred.toml`,
189
+ so `zagred dev` always finds the server on the port it polls.
190
+
191
+ ### Window options
192
+
193
+ ```python
194
+ app = zagred.App(
195
+ title="My App",
196
+ size=(1200, 800),
197
+ min_size=(640, 480),
198
+ position=(100, 100), # omit to let the OS place it
199
+ resizable=True,
200
+ fullscreen=False,
201
+ maximized=False,
202
+ frameless=False, # no title bar
203
+ easy_drag=True, # frameless windows stay draggable
204
+ on_top=False,
205
+ focus=True,
206
+ shadow=False,
207
+ confirm_close=False, # ask before closing
208
+ transparent=False,
209
+ text_select=False,
210
+ zoomable=False,
211
+ vibrancy=False, # macOS
212
+ background_color="#ffffff",
213
+ icon="my_app/resources/icon.png",
214
+ menu=[],
215
+ )
216
+ ```
217
+
218
+ Control it at runtime:
219
+
220
+ ```python
221
+ app.minimize() / app.maximize() / app.restore()
222
+ app.toggle_fullscreen()
223
+ app.resize(900, 600) / app.move(50, 50)
224
+ app.set_title("Renamed")
225
+ app.show() / app.hide() / app.destroy()
226
+ app.load_url("http://localhost:5173")
227
+ app.evaluate_js("document.title")
228
+ app.create_file_dialog(allow_multiple=False) # native picker
229
+ ```
230
+
231
+ Lifecycle hooks:
232
+
233
+ ```python
234
+ @app.on_start
235
+ def boot(): ... # GUI loop is up
236
+
237
+ @app.on_loaded
238
+ def ready(): ... # page finished loading
239
+
240
+ @app.on_closing
241
+ def guard():
242
+ return False # cancel the close
243
+
244
+ @app.on_closed
245
+ def bye(): ...
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Building
251
+
252
+ ```bash
253
+ uv sync --group build # adds Nuitka (+ patchelf on Linux)
254
+ uv run zagred build # auto target for this OS
255
+ ```
256
+
257
+ | `--target` | Nuitka flags | Output |
258
+ |---|---|---|
259
+ | `auto` *(default)* | picks by OS | `.AppImage` / `.exe` / `.app` |
260
+ | `onefile` | `--mode=onefile` | single binary |
261
+ | `standalone` | `--mode=standalone` | folder with binary + libs |
262
+ | `exe` | `--mode=standalone --windows-create-installer` | NSIS `-setup.exe` |
263
+ | `appimage` | `--mode=app-dist --linux-create-installer` | `.AppImage` |
264
+ | `app` | `--mode=app` | macOS `.app` |
265
+ | `dmg` | `--mode=app --macos-create-installer` | `.dmg` |
266
+
267
+ Each target writes to its own directory, so building one never overwrites another:
268
+
269
+ ```
270
+ dist/
271
+ ├── appimage/
272
+ │ ├── my-app.AppImage
273
+ │ └── my-app.dist/
274
+ ├── onefile/
275
+ │ └── my-app
276
+ └── exe/
277
+ └── my-app-setup.exe
278
+ ```
279
+
280
+ On Windows, some antivirus engines flag the `onefile` stub as suspicious. If
281
+ that happens, ship `--target standalone` instead — same app, packaged as an
282
+ installer, and it does not trigger the heuristic.
283
+
284
+ ### What `zagred build` does
285
+
286
+ ```
287
+ 1. npm run build (in web/, if [ui] build is set)
288
+ 2. write .zagred/build/<name>.py static import of your App + boot()
289
+ 3. nuitka --include-data-dir=web/dist=ui
290
+ 4. wrap the .dist folder into the installer for the chosen target
291
+ 5. write everything to dist/<target>/
292
+ ```
293
+
294
+ Step 2 imports your App statically on purpose. Nuitka bundles only what it can
295
+ see through imports, so a dynamic `importlib.import_module("my_app.app")` would
296
+ leave your package out and crash at startup.
297
+
298
+ ### Keeping the machine usable
299
+
300
+ Compilation is CPU-hungry. Default is every core.
301
+
302
+ ```toml
303
+ [build]
304
+ jobs = 2 # at most 2 parallel C compiles
305
+ low_memory = true # optional: fewer jobs + lighter compiler options
306
+ ```
307
+
308
+ ```bash
309
+ uv run zagred build --jobs 2 # one-off override
310
+ ```
311
+
312
+ | `--jobs` | Effect |
313
+ |---|---|
314
+ | unset | every core (Nuitka default) |
315
+ | `2` | 2 parallel C compiles |
316
+ | `-1` | CPU count minus 1 |
317
+ | `1` | serial |
318
+
319
+ Measured on an 8-core laptop: default ≈ 6m47s wall / 39m30s CPU (~6 cores busy);
320
+ `--jobs=2` ≈ 11 min with load ~3.
321
+
322
+ ### `--debug` is for development only
323
+
324
+ `zagred build --debug` bakes `debug=True` into the compiled app. pywebview then
325
+ enables developer extras, opens the WebView inspector, and stops suppressing the
326
+ right-click context menu. Fine for testing a packaged build, never for release.
327
+ The command warns when you do it.
328
+
329
+ ---
330
+
331
+ ## How it works
332
+
333
+ Every JS → Python call goes through one door:
334
+
335
+ ```
336
+ zagred.call(name, args)
337
+ → window.pywebview.api.call(name, args) # the only exposed method
338
+ → Bridge.call() → your @expose function
339
+ → {ok: true, data} | {ok: false, error: {code, message}}
340
+ → resolved value, or a thrown Error(code, message)
341
+ ```
342
+
343
+ Your code never touches `window.pywebview`. The `window.zagred` shim is injected
344
+ on `before_load` from a worker thread — calling `evaluate_js` directly on that
345
+ blocking event deadlocks the GTK main loop and leaves the window white.
346
+
347
+ ## Requirements
348
+
349
+ | OS | Needs |
350
+ |---|---|
351
+ | Linux | `libwebkit2gtk-4.1`, `python3-gi`, venv created with `--system-site-packages`; `patchelf` comes with `--group build` |
352
+ | Windows | WebView2 runtime (preinstalled on Windows 11) |
353
+ | macOS | nothing extra |
354
+
355
+ Linux uses the GTK backend on purpose: Qt adds 80–150 MB to every build.
356
+ `appimagetool` is downloaded and cached by Nuitka when missing.
357
+ Cross-compiling is impossible — build each OS on that OS, or in CI.
358
+
359
+ ## Troubleshooting
360
+
361
+ **`uv sync` says zagred is not found in the registry** — not on PyPI yet. Projects
362
+ created by `zagred new` get a `[tool.uv.sources]` entry pointing at the local
363
+ checkout. If you copied a project from elsewhere:
364
+
365
+ ```toml
366
+ [tool.uv.sources]
367
+ zagred = { path = "/path/to/zagred", editable = true }
368
+ ```
369
+
370
+ **`zagred dev` says GTK/QT cannot be loaded** — the bindings exist on the system
371
+ but this interpreter cannot see them:
372
+
373
+ ```bash
374
+ sudo apt install python3-gi gir1.2-webkit2-4.1
375
+ rm -rf .venv
376
+ uv venv --python /usr/bin/python3 --system-site-packages
377
+ uv sync
378
+ ```
379
+
380
+ **`uv` warns that `VIRTUAL_ENV` does not match** — you activated another project's
381
+ venv. `deactivate`; `uv run` picks the right one.
382
+
383
+ **`zagred doctor` says nuitka MISSING** — `uv sync --group build`.
384
+
385
+ **A built app opens the inspector** — it was built with `--debug`. Rebuild without it.
386
+
387
+ ---
388
+
389
+ ## Milestones
390
+
391
+ ### ✅ M0 — Spike
392
+ Bridge roundtrip, error envelope, async commands, events via `run_js`,
393
+ working `--mode=onefile` binary, assets resolved from the onefile temp dir.
394
+
395
+ ### ✅ M1 — Runtime
396
+ `zagred.toml` parser, `@expose`, JS shim, event bus, scaffolds that run,
397
+ `zagred dev`, actionable errors instead of raw tracebacks, 38 unit tests.
398
+
399
+ ### ✅ M2 — uv toolchain
400
+ Generated projects are uv projects; zagred resolved from the local checkout via
401
+ `[tool.uv.sources]` (editable); `.python-version` pinned to 3.12; `build`
402
+ dependency group carries nuitka + patchelf; `zagred new` prints next steps.
403
+
404
+ ### ✅ M3 — Build
405
+ Six Nuitka targets, asset resolver for dev/standalone/onefile, per-target output
406
+ directories, `--jobs` / `low_memory`, `zagred doctor`, artifacts named after the app.
407
+
408
+ ### 🟡 M4 — Ecosystem
409
+ - [x] `zagred new` wizard, `--template` / `--lang` / `-y`
410
+ - [x] Templates: vanilla, React, Vue, Solid × JS/TS
411
+ - [x] Vite HMR dev path, verified against a real React project
412
+ - [x] Typed bridge for TypeScript
413
+ - [x] Full window option set, lifecycle hooks, window control methods
414
+ - [ ] Multi-window (one `Bridge` per window)
415
+ - [ ] Plugin registry via `entry_points`
416
+ - [ ] `zagred add ui <kit>`
417
+
418
+ ### ⬜ M5 — Security
419
+ - [ ] `capabilities.toml` + permission check before dispatch
420
+ - [ ] Default CSP
421
+ - [ ] Path traversal guard on the resource resolver
422
+
423
+ ### ⬜ M6 — Release
424
+ - [ ] Publish to PyPI
425
+ - [ ] AppImage test on a clean VM (GTK typelibs)
426
+ - [ ] CI matrix (GitHub Actions, `Nuitka-Action`)
427
+ - [ ] Docs site, example apps, stable semver
428
+
429
+ ---
430
+
431
+ ## Fixed so far
432
+
433
+ 22 bugs found and fixed while building this. The ones that would have shipped:
434
+
435
+ | # | Bug |
436
+ |---|---|
437
+ | B4 | Blank window from a built binary — onefile assets resolved from the wrong directory |
438
+ | B7 | `uv sync` failed in every generated project — zagred was declared from PyPI |
439
+ | B11 | GTK not visible from uv's Python — venv must use `--system-site-packages` |
440
+ | B12 | White window for seconds — `evaluate_js` deadlocked the blocking `before_load` event |
441
+ | B15 | Terminal broken after closing the window — orphaned `vite` held the TTY |
442
+ | B19 | Packaged app crashed: `No module named my_app` — dynamic import invisible to Nuitka |
443
+ | B21 | Building one target deleted another's artifacts — shared output dir |
444
+ | B22 | Release AppImage opened the WebView inspector — `--debug` baked into the build |
445
+
446
+ Full list with causes and fixes: `git log` and the test suite in `tests/test_core.py`.
447
+
448
+ ---
449
+
450
+ ## Development
451
+
452
+ ```bash
453
+ uv sync --extra build
454
+ uv run python -m unittest discover -s tests # 38 tests
455
+ uv run zagred doctor
456
+ ```
457
+
458
+ ## License
459
+
460
+ MIT — see [LICENSE](LICENSE). Use it, fork it, ship it in commercial closed-source
461
+ apps; just keep the copyright notice.
462
+
463
+ Apps built with `zagred build` compile Zagred, pywebview, bottle, proxy_tools and
464
+ CPython into the binary, so your distribution should also include
465
+ [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) and the PSF licence (shipped with
466
+ Python). Nuitka is AGPL-3.0, but its Runtime Library Exception explicitly allows
467
+ closed-source output, and Zagred never bundles it.