struct2ui 0.1.0__tar.gz → 0.3.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.
Files changed (49) hide show
  1. struct2ui-0.3.0/PKG-INFO +362 -0
  2. struct2ui-0.3.0/README.md +309 -0
  3. {struct2ui-0.1.0 → struct2ui-0.3.0}/pyproject.toml +1 -1
  4. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/__init__.py +1 -1
  5. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/editor.py +418 -64
  6. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/exporters/bin_emitter.py +2 -1
  7. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/exporters/c_emitter.py +10 -11
  8. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/schema.py +207 -15
  9. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/ui/renderers.py +2 -13
  10. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/ui/tables.py +235 -207
  11. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/ui/widgets.py +103 -4
  12. struct2ui-0.3.0/src/struct2ui.egg-info/PKG-INFO +362 -0
  13. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui.egg-info/SOURCES.txt +2 -0
  14. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_c_emitter.py +122 -69
  15. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_c_parser.py +26 -0
  16. struct2ui-0.3.0/tests/test_choices.py +175 -0
  17. struct2ui-0.3.0/tests/test_editor.py +255 -0
  18. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_float_precision.py +20 -1
  19. struct2ui-0.3.0/tests/test_int_checkbox.py +47 -0
  20. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_save.py +58 -3
  21. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_value_readback.py +36 -1
  22. struct2ui-0.1.0/PKG-INFO +0 -167
  23. struct2ui-0.1.0/README.md +0 -114
  24. struct2ui-0.1.0/src/struct2ui.egg-info/PKG-INFO +0 -167
  25. struct2ui-0.1.0/tests/test_editor.py +0 -75
  26. {struct2ui-0.1.0 → struct2ui-0.3.0}/LICENSE +0 -0
  27. {struct2ui-0.1.0 → struct2ui-0.3.0}/setup.cfg +0 -0
  28. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/exporters/__init__.py +0 -0
  29. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/exporters/c_parser.py +0 -0
  30. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/exporters/elf_verifier.py +0 -0
  31. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/exporters/json_format.py +0 -0
  32. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/c2j.png +0 -0
  33. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/elf.png +0 -0
  34. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/export_notes_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  35. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/flowchart_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  36. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/refresh_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  37. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/report_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  38. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/save_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  39. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/save_as_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  40. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/settings_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  41. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/icons/widgets_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
  42. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui/ui/__init__.py +0 -0
  43. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui.egg-info/dependency_links.txt +0 -0
  44. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui.egg-info/requires.txt +0 -0
  45. {struct2ui-0.1.0 → struct2ui-0.3.0}/src/struct2ui.egg-info/top_level.txt +0 -0
  46. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_bin_emitter.py +0 -0
  47. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_elf_verifier.py +0 -0
  48. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_json_format.py +0 -0
  49. {struct2ui-0.1.0 → struct2ui-0.3.0}/tests/test_load_report.py +0 -0
@@ -0,0 +1,362 @@
1
+ Metadata-Version: 2.4
2
+ Name: struct2ui
3
+ Version: 0.3.0
4
+ Summary: Render C struct / JSON schema as editable PySide6 UI, export to C/JSON/bin
5
+ Author: Jay
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Jay
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: qt,pyside6,gui,c-struct,json-schema,code-generation,parameter-tuning
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.9
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Topic :: Software Development :: Code Generators
38
+ Classifier: Topic :: Software Development :: User Interfaces
39
+ Requires-Python: >=3.9
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: Qt.py>=1.3
43
+ Provides-Extra: pyside6
44
+ Requires-Dist: PySide6>=6.4; extra == "pyside6"
45
+ Provides-Extra: pyqt6
46
+ Requires-Dist: PyQt6>=6.4; extra == "pyqt6"
47
+ Provides-Extra: elf
48
+ Requires-Dist: pyelftools>=0.29; extra == "elf"
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest>=8; extra == "dev"
51
+ Requires-Dist: pyelftools>=0.29; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ # struct2ui
55
+
56
+ Render C structs / JSON schemas as an **editable PySide6 UI**, and convert freely between **C / JSON / bin**. Built for algorithm parameter tuning: describe a C interface in JSON, auto-generate a Qt form, edit it, then export back to C source, JSON, or binary.
57
+
58
+ ## Features
59
+
60
+ - **JSON → UI**: describe C structs / enums / arrays with minimal JSON and auto-render Qt widgets (int→QSpinBox, float→QDoubleSpinBox, enum→QComboBox, etc.).
61
+ - **Embeddable**: `StructEditor` is a plain `QWidget` that drops into any PySide6 / PyQt UI.
62
+ - **Multi-format export**: edited results export to C source, JSON, or binary; C source can also be parsed back into a schema.
63
+ - **Validation**: keyword spell-checking (with “Did you mean X?” hints), semantic audits (`min<=max`, `step>0`), and pipeline cross-validation.
64
+ - **Qt-binding agnostic**: built on [Qt.py](https://github.com/mottosso/Qt.py) — works with PySide6 / PyQt6 / PySide2 / PyQt5.
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ pip install struct2ui
70
+
71
+ # Pick a Qt binding (choose one)
72
+ pip install "struct2ui[pyside6]"
73
+ pip install "struct2ui[pyqt6]"
74
+
75
+ # Enable ELF layout verification (optional)
76
+ pip install "struct2ui[elf]"
77
+ ```
78
+
79
+ > The library itself only depends on `Qt.py`; you must install a Qt binding (PySide6 / PyQt6 / etc.) yourself.
80
+ > ELF verification depends on `pyelftools`, installed via the `[elf]` extra.
81
+
82
+ ## Quick Start
83
+
84
+ ```python
85
+ from Qt import QtWidgets
86
+ from struct2ui import StructEditor
87
+
88
+ app = QtWidgets.QApplication([])
89
+
90
+ editor = StructEditor(
91
+ flow_file="abc.json", # pipeline definition file (optional)
92
+ cfg_dir="cfg_t", # modules dir: *.json holding struct/enum/typedef (optional)
93
+ )
94
+ editor.resize(480, 600)
95
+ editor.show()
96
+
97
+ app.exec_()
98
+ ```
99
+
100
+ ## JSON Schema Reference
101
+
102
+ This is the core of the library: you describe your C types in JSON and the UI is
103
+ generated from it. Files live in the **modules directory** (`cfg_dir`), one
104
+ `*.json` per logical group. Each file is a flat object whose keys are either
105
+ **type names** (your structs / enums) or the reserved key `typedefs`.
106
+
107
+ ### File layout
108
+
109
+ ```jsonc
110
+ {
111
+ // optional free-form metadata (ignored by the loader, allowed anywhere)
112
+ "version": "1.0",
113
+ "description": "audio EQ parameters",
114
+
115
+ // type aliases: map a custom C type to a primitive
116
+ "typedefs": { "gain_t": "int32_t", "freq_t": "float" },
117
+
118
+ // a struct definition
119
+ "eq_cfg_t": {
120
+ "type": "struct",
121
+ "items": [
122
+ { "name": "enabled", "type": "uint8_t", "value": 1 },
123
+ { "name": "gain", "type": "gain_t", "value": 0,
124
+ "min": -12, "max": 12, "step": 1, "unit": "dB",
125
+ "tip": "output gain", "when": { "enabled": 1 } }
126
+ ]
127
+ },
128
+
129
+ // an enum definition
130
+ "mode_t": {
131
+ "type": "enum",
132
+ "items": { "OFF": 0, "LOW": 1, "HIGH": 2 }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### Top-level blocks
138
+
139
+ | Block type | Keys | Notes |
140
+ | --- | --- | --- |
141
+ | `struct` | `type`, `items` (list of field specs) | `items` is required |
142
+ | `enum` | `type`, `items` (object `{NAME: value}`) | reads back the enumerator name |
143
+ | `typedefs` | object `{alias: real_type}` | resolved before field building |
144
+
145
+ Free-form metadata keys are allowed and ignored at any level: `version`,
146
+ `description`, `author`, `comment`, `note`.
147
+
148
+ ### Field properties (inside a struct's `items`)
149
+
150
+ Each entry in `items` is a **field spec**. Only the following keys are
151
+ recognized — any other key triggers an "unknown keyword" error (with a
152
+ "Did you mean …?" hint):
153
+
154
+ | Property | Applies to | Description |
155
+ | --- | --- | --- |
156
+ | `name` | all | **Required.** C field name. |
157
+ | `type` | all | **Required.** C type or a `typedefs` alias (e.g. `int32_t`, `float`, `char`, an enum/struct name). |
158
+ | `value` | all | Default value. For arrays, a list (per element) or a scalar (applied to every element). |
159
+ | `count` | arrays | Makes the field an array. Integer, a `#define` name, an expression (`N - 1`), or a list for multi-dim (`[3, 4]`, stored flat as 12). |
160
+ | `min` / `max` | int, float | Numeric bounds. Validated `min <= max`. Drive spin-box / slider / dial ranges. |
161
+ | `step` | int, float | Increment, must be `> 0`. |
162
+ | `decimals` | float | Number of fractional digits shown/stored. Non-negative integer; defaults to digits implied by `step`. |
163
+ | `unit` | all | Unit suffix shown in the label, e.g. `gain (dB)`. |
164
+ | `tip` | all | Tooltip text. |
165
+ | `choices` | int, float, `char[N]` | Discrete value set rendered as a combo (see below). |
166
+ | `when` | all | Conditional enable/disable (see below). |
167
+ | `widget` | all | Override the auto-picked editor (see table below). |
168
+
169
+ ### `widget` values
170
+
171
+ When omitted, the widget is inferred from `type` (and from `min/max`,
172
+ `choices`). Override it explicitly with `widget`:
173
+
174
+ | `widget` | Valid for | Renders as |
175
+ | --- | --- | --- |
176
+ | `checkbox` | int | Check box (also auto-picked when `min:0, max:1`). |
177
+ | `toggle` | bool/int | Toggle push-button. |
178
+ | `combo` | int, float, `char[N]` | Combo box; pair with `choices`. |
179
+ | `slider` | int | Slider with min/max labels (requires `min` & `max`). |
180
+ | `dial` | int | Rotary dial popup (requires `min` & `max`). |
181
+ | `file` | array | Path label + Browse button; loads array values from a text file. |
182
+ | `multiline` | array | Single-line shell that pops up a multi-line editor. |
183
+ | `table` | struct array | Force grid layout (one column per struct member). |
184
+
185
+ Numeric fields without an explicit `widget` become a `QSpinBox` /
186
+ `QDoubleSpinBox`; scalar arrays become a comma-separated line edit.
187
+
188
+ ### `choices` (discrete values)
189
+
190
+ A non-enum integer / float / `char[N]` field can be constrained to a fixed set.
191
+ Two forms are accepted:
192
+
193
+ ```jsonc
194
+ "choices": [0, 1, 2, 3] // label = str(value)
195
+ "choices": [{ "Off": 0 }, { "Low": 1 }, { "High": 2 }] // explicit labels
196
+ ```
197
+
198
+ The combo **reads back the underlying value** (not the label), so C / JSON / bin
199
+ export still emits the number (or string for `char[N]`). Audits enforce: each
200
+ value within `[min, max]` when declared, and `value` (the default) must be one
201
+ of the choices.
202
+
203
+ ### `when` (conditional enable/disable)
204
+
205
+ A field can be greyed out unless other fields hold specific values. `when` is an
206
+ object mapping a **sibling field name** to its required value; all entries must
207
+ match (logical AND):
208
+
209
+ ```jsonc
210
+ { "name": "cutoff", "type": "float", "when": { "enabled": 1, "mode": "HIGH" } }
211
+ ```
212
+
213
+ The dependency name is matched against the dotted leaf name in scope. When a
214
+ dependency is not found, the field stays enabled.
215
+
216
+ ### `count` expressions & multi-dimensional arrays
217
+
218
+ `count` may be an integer, a `#define` constant name, or an integer expression
219
+ of those using `+ - * / % ()` (e.g. `MAX_BAND_NUM - 1`). A list makes a
220
+ multi-dimensional array that is **stored flat** to match the C ABI:
221
+
222
+ ```jsonc
223
+ { "name": "matrix", "type": "float", "count": [3, 4] } // 12 contiguous floats, shown as [3][4]
224
+ ```
225
+
226
+ `char[N]` is treated as a C string by default (use `widget` to override).
227
+
228
+ ### Validation
229
+
230
+ Loading runs three layers of checks, surfaced in the **load report** panel:
231
+
232
+ - **Keyword spelling** — unknown keys report "Did you mean `tip`?" style hints.
233
+ - **Semantic audits** — `min <= max`, `step > 0`, `count > 0`, choices within
234
+ bounds, default is one of the choices.
235
+ - **Pipeline cross-validation** — values in the pipeline file are checked
236
+ against the schema (range, choices, enum membership).
237
+
238
+ ## Embedding into an Existing UI
239
+
240
+ `StructEditor` is a regular `QWidget`; just put it into a layout:
241
+
242
+ ```python
243
+ from Qt import QtWidgets
244
+ from struct2ui import StructEditor
245
+
246
+ class MyWindow(QtWidgets.QMainWindow):
247
+ def __init__(self):
248
+ super().__init__()
249
+ editor = StructEditor(
250
+ "abc.json", "cfg_t",
251
+ settings_org="MyCompany", # custom QSettings scope to
252
+ settings_app="MyApp", # avoid clashing with the host app
253
+ )
254
+ self.setCentralWidget(editor)
255
+ ```
256
+
257
+ ### Constructor Parameters
258
+
259
+ | Parameter | Description |
260
+ | --- | --- |
261
+ | `flow_file` | Path to the pipeline JSON file (pipeline definition), defaults to `None`; pick it in the UI when omitted |
262
+ | `cfg_dir` | Modules directory holding `*.json` (struct / enum / typedef definitions), defaults to `None`; pick it in the UI when omitted |
263
+ | `parent` | Qt parent object, defaults to `None` |
264
+ | `elf_path` | Path to an ELF for layout verification, defaults to `None`; pick it in the UI when omitted |
265
+ | `appearance` | Button appearance overrides, `{key: {'mode': ..., 'icon': ..., 'text': ...}}` |
266
+ | `settings_org` | QSettings organization name, defaults to `'struct2ui'` |
267
+ | `settings_app` | QSettings application name, defaults to `'StructEditor'` |
268
+ | `toolbar_orientation` | Toolbar layout, `'horizontal'` (default) or `'vertical'` |
269
+ | `custom_button_position` | Where `add_custom_button()` drops buttons relative to the built-ins: `'end'` (after, default) or `'start'` (before / far left) |
270
+
271
+ ## Custom Toolbar Buttons
272
+
273
+ Host applications can add their own buttons to the top path bar via
274
+ `add_custom_button()`. All custom buttons form a single group, separated from
275
+ the built-in buttons by a divider that is added automatically — you only choose
276
+ which side they sit on (once, via the `custom_button_position` constructor arg).
277
+
278
+ ```python
279
+ editor = StructEditor("abc.json", "cfg_t", custom_button_position="end")
280
+
281
+ def on_send():
282
+ print(editor.current_values())
283
+
284
+ btn = editor.add_custom_button(
285
+ "Send", # tooltip / accessible label (shown when no icon)
286
+ on_send, # callable invoked on click (no arguments)
287
+ icon="icons/send.png", # optional; falls back to text when missing
288
+ checkable=False, # optional; make the button toggleable
289
+ )
290
+ # `btn` is the created QPushButton, returned so you can tweak it further.
291
+ ```
292
+
293
+ | Parameter | Description |
294
+ | --- | --- |
295
+ | `text` | Tooltip / accessible label, also shown when no icon is given. **Required.** |
296
+ | `on_click` | Callable invoked on click, takes no arguments. **Required.** |
297
+ | `icon` | Path to an icon file; falls back to `text` when the path is missing/empty. |
298
+ | `checkable` | Make the button toggleable, defaults to `False`. |
299
+
300
+ Returns the created `QPushButton`.
301
+
302
+ ## Programmatic API
303
+
304
+ Beyond the UI, `StructEditor` exposes methods to read state and drive exports
305
+ from code (e.g. wired to a custom button):
306
+
307
+ | Method | Description |
308
+ | --- | --- |
309
+ | `current_values() -> dict` | Live editor values of the active section, keyed by field name (empty dict when no parameter section is shown). |
310
+ | `current_paths() -> dict` | Current `{'modules', 'pipeline', 'elf'}` file paths; an empty string means that slot is unset. |
311
+ | `export_bin(path, show_dialogs=False) -> bool` | Write the current pipeline to a `.bin` at `path` — the programmatic equivalent of clicking Export → Binary. Returns `True` when there are no ELF layout errors (or no ELF is selected), `False` otherwise. |
312
+
313
+ `export_bin` verifies the ELF layout first when an ELF is selected (the file is
314
+ still written, matching the UI flow). With `show_dialogs=True` it pops the same
315
+ success / ELF-mismatch / failure dialogs the UI shows; with the default
316
+ `show_dialogs=False` no dialogs appear and render/write failures are raised as
317
+ exceptions for the caller to handle.
318
+
319
+ ```python
320
+ paths = editor.current_paths() # {'modules': ..., 'pipeline': ..., 'elf': ...}
321
+ name = paths['pipeline'] # derive an output name from the pipeline
322
+ ok = editor.export_bin("out/speech.bin") # True when ELF layout is clean
323
+ ```
324
+
325
+ ## Export API
326
+
327
+ The low-level export functions can be used standalone, without any UI:
328
+
329
+ ```python
330
+ from struct2ui.schema import SchemaRegistry
331
+ from struct2ui.exporters import (
332
+ emit_c, # sections + registry -> C source string
333
+ dumps_json, # -> JSON string
334
+ emit_bin, # -> binary bytes
335
+ merge_abi, # merge ABI info
336
+ verify_sections, # verify .bin layout against an ELF
337
+ parse_c_source, # C source -> parse result
338
+ build_schema_dict,
339
+ )
340
+ ```
341
+
342
+ ## Architecture
343
+
344
+ | Layer | Module | Responsibility |
345
+ | --- | --- | --- |
346
+ | Schema (pure data, no Qt) | `struct2ui.schema` | Parse `*.json` into a Field tree; spell-checking, semantic audits, pipeline cross-validation |
347
+ | UI rendering | `struct2ui.ui` | `WidgetFactory`, `FormRenderer`/`TreeRenderer`, array tables, `when` conditional binding |
348
+ | Export | `struct2ui.exporters` | C / JSON / bin export, C source reverse parsing, ELF verification |
349
+ | Top-level widget | `struct2ui.StructEditor` | Path bar / action buttons / content area; load report panel; QSettings path memory |
350
+
351
+ ## Development
352
+
353
+ ```bash
354
+ pip install -e ".[dev]" --no-build-isolation
355
+ python -m pytest
356
+ ```
357
+
358
+ Tests live under `tests/` and drive real Qt widgets headlessly on the offscreen platform.
359
+
360
+ ## License
361
+
362
+ [MIT](LICENSE) © Jay
@@ -0,0 +1,309 @@
1
+ # struct2ui
2
+
3
+ Render C structs / JSON schemas as an **editable PySide6 UI**, and convert freely between **C / JSON / bin**. Built for algorithm parameter tuning: describe a C interface in JSON, auto-generate a Qt form, edit it, then export back to C source, JSON, or binary.
4
+
5
+ ## Features
6
+
7
+ - **JSON → UI**: describe C structs / enums / arrays with minimal JSON and auto-render Qt widgets (int→QSpinBox, float→QDoubleSpinBox, enum→QComboBox, etc.).
8
+ - **Embeddable**: `StructEditor` is a plain `QWidget` that drops into any PySide6 / PyQt UI.
9
+ - **Multi-format export**: edited results export to C source, JSON, or binary; C source can also be parsed back into a schema.
10
+ - **Validation**: keyword spell-checking (with “Did you mean X?” hints), semantic audits (`min<=max`, `step>0`), and pipeline cross-validation.
11
+ - **Qt-binding agnostic**: built on [Qt.py](https://github.com/mottosso/Qt.py) — works with PySide6 / PyQt6 / PySide2 / PyQt5.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install struct2ui
17
+
18
+ # Pick a Qt binding (choose one)
19
+ pip install "struct2ui[pyside6]"
20
+ pip install "struct2ui[pyqt6]"
21
+
22
+ # Enable ELF layout verification (optional)
23
+ pip install "struct2ui[elf]"
24
+ ```
25
+
26
+ > The library itself only depends on `Qt.py`; you must install a Qt binding (PySide6 / PyQt6 / etc.) yourself.
27
+ > ELF verification depends on `pyelftools`, installed via the `[elf]` extra.
28
+
29
+ ## Quick Start
30
+
31
+ ```python
32
+ from Qt import QtWidgets
33
+ from struct2ui import StructEditor
34
+
35
+ app = QtWidgets.QApplication([])
36
+
37
+ editor = StructEditor(
38
+ flow_file="abc.json", # pipeline definition file (optional)
39
+ cfg_dir="cfg_t", # modules dir: *.json holding struct/enum/typedef (optional)
40
+ )
41
+ editor.resize(480, 600)
42
+ editor.show()
43
+
44
+ app.exec_()
45
+ ```
46
+
47
+ ## JSON Schema Reference
48
+
49
+ This is the core of the library: you describe your C types in JSON and the UI is
50
+ generated from it. Files live in the **modules directory** (`cfg_dir`), one
51
+ `*.json` per logical group. Each file is a flat object whose keys are either
52
+ **type names** (your structs / enums) or the reserved key `typedefs`.
53
+
54
+ ### File layout
55
+
56
+ ```jsonc
57
+ {
58
+ // optional free-form metadata (ignored by the loader, allowed anywhere)
59
+ "version": "1.0",
60
+ "description": "audio EQ parameters",
61
+
62
+ // type aliases: map a custom C type to a primitive
63
+ "typedefs": { "gain_t": "int32_t", "freq_t": "float" },
64
+
65
+ // a struct definition
66
+ "eq_cfg_t": {
67
+ "type": "struct",
68
+ "items": [
69
+ { "name": "enabled", "type": "uint8_t", "value": 1 },
70
+ { "name": "gain", "type": "gain_t", "value": 0,
71
+ "min": -12, "max": 12, "step": 1, "unit": "dB",
72
+ "tip": "output gain", "when": { "enabled": 1 } }
73
+ ]
74
+ },
75
+
76
+ // an enum definition
77
+ "mode_t": {
78
+ "type": "enum",
79
+ "items": { "OFF": 0, "LOW": 1, "HIGH": 2 }
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Top-level blocks
85
+
86
+ | Block type | Keys | Notes |
87
+ | --- | --- | --- |
88
+ | `struct` | `type`, `items` (list of field specs) | `items` is required |
89
+ | `enum` | `type`, `items` (object `{NAME: value}`) | reads back the enumerator name |
90
+ | `typedefs` | object `{alias: real_type}` | resolved before field building |
91
+
92
+ Free-form metadata keys are allowed and ignored at any level: `version`,
93
+ `description`, `author`, `comment`, `note`.
94
+
95
+ ### Field properties (inside a struct's `items`)
96
+
97
+ Each entry in `items` is a **field spec**. Only the following keys are
98
+ recognized — any other key triggers an "unknown keyword" error (with a
99
+ "Did you mean …?" hint):
100
+
101
+ | Property | Applies to | Description |
102
+ | --- | --- | --- |
103
+ | `name` | all | **Required.** C field name. |
104
+ | `type` | all | **Required.** C type or a `typedefs` alias (e.g. `int32_t`, `float`, `char`, an enum/struct name). |
105
+ | `value` | all | Default value. For arrays, a list (per element) or a scalar (applied to every element). |
106
+ | `count` | arrays | Makes the field an array. Integer, a `#define` name, an expression (`N - 1`), or a list for multi-dim (`[3, 4]`, stored flat as 12). |
107
+ | `min` / `max` | int, float | Numeric bounds. Validated `min <= max`. Drive spin-box / slider / dial ranges. |
108
+ | `step` | int, float | Increment, must be `> 0`. |
109
+ | `decimals` | float | Number of fractional digits shown/stored. Non-negative integer; defaults to digits implied by `step`. |
110
+ | `unit` | all | Unit suffix shown in the label, e.g. `gain (dB)`. |
111
+ | `tip` | all | Tooltip text. |
112
+ | `choices` | int, float, `char[N]` | Discrete value set rendered as a combo (see below). |
113
+ | `when` | all | Conditional enable/disable (see below). |
114
+ | `widget` | all | Override the auto-picked editor (see table below). |
115
+
116
+ ### `widget` values
117
+
118
+ When omitted, the widget is inferred from `type` (and from `min/max`,
119
+ `choices`). Override it explicitly with `widget`:
120
+
121
+ | `widget` | Valid for | Renders as |
122
+ | --- | --- | --- |
123
+ | `checkbox` | int | Check box (also auto-picked when `min:0, max:1`). |
124
+ | `toggle` | bool/int | Toggle push-button. |
125
+ | `combo` | int, float, `char[N]` | Combo box; pair with `choices`. |
126
+ | `slider` | int | Slider with min/max labels (requires `min` & `max`). |
127
+ | `dial` | int | Rotary dial popup (requires `min` & `max`). |
128
+ | `file` | array | Path label + Browse button; loads array values from a text file. |
129
+ | `multiline` | array | Single-line shell that pops up a multi-line editor. |
130
+ | `table` | struct array | Force grid layout (one column per struct member). |
131
+
132
+ Numeric fields without an explicit `widget` become a `QSpinBox` /
133
+ `QDoubleSpinBox`; scalar arrays become a comma-separated line edit.
134
+
135
+ ### `choices` (discrete values)
136
+
137
+ A non-enum integer / float / `char[N]` field can be constrained to a fixed set.
138
+ Two forms are accepted:
139
+
140
+ ```jsonc
141
+ "choices": [0, 1, 2, 3] // label = str(value)
142
+ "choices": [{ "Off": 0 }, { "Low": 1 }, { "High": 2 }] // explicit labels
143
+ ```
144
+
145
+ The combo **reads back the underlying value** (not the label), so C / JSON / bin
146
+ export still emits the number (or string for `char[N]`). Audits enforce: each
147
+ value within `[min, max]` when declared, and `value` (the default) must be one
148
+ of the choices.
149
+
150
+ ### `when` (conditional enable/disable)
151
+
152
+ A field can be greyed out unless other fields hold specific values. `when` is an
153
+ object mapping a **sibling field name** to its required value; all entries must
154
+ match (logical AND):
155
+
156
+ ```jsonc
157
+ { "name": "cutoff", "type": "float", "when": { "enabled": 1, "mode": "HIGH" } }
158
+ ```
159
+
160
+ The dependency name is matched against the dotted leaf name in scope. When a
161
+ dependency is not found, the field stays enabled.
162
+
163
+ ### `count` expressions & multi-dimensional arrays
164
+
165
+ `count` may be an integer, a `#define` constant name, or an integer expression
166
+ of those using `+ - * / % ()` (e.g. `MAX_BAND_NUM - 1`). A list makes a
167
+ multi-dimensional array that is **stored flat** to match the C ABI:
168
+
169
+ ```jsonc
170
+ { "name": "matrix", "type": "float", "count": [3, 4] } // 12 contiguous floats, shown as [3][4]
171
+ ```
172
+
173
+ `char[N]` is treated as a C string by default (use `widget` to override).
174
+
175
+ ### Validation
176
+
177
+ Loading runs three layers of checks, surfaced in the **load report** panel:
178
+
179
+ - **Keyword spelling** — unknown keys report "Did you mean `tip`?" style hints.
180
+ - **Semantic audits** — `min <= max`, `step > 0`, `count > 0`, choices within
181
+ bounds, default is one of the choices.
182
+ - **Pipeline cross-validation** — values in the pipeline file are checked
183
+ against the schema (range, choices, enum membership).
184
+
185
+ ## Embedding into an Existing UI
186
+
187
+ `StructEditor` is a regular `QWidget`; just put it into a layout:
188
+
189
+ ```python
190
+ from Qt import QtWidgets
191
+ from struct2ui import StructEditor
192
+
193
+ class MyWindow(QtWidgets.QMainWindow):
194
+ def __init__(self):
195
+ super().__init__()
196
+ editor = StructEditor(
197
+ "abc.json", "cfg_t",
198
+ settings_org="MyCompany", # custom QSettings scope to
199
+ settings_app="MyApp", # avoid clashing with the host app
200
+ )
201
+ self.setCentralWidget(editor)
202
+ ```
203
+
204
+ ### Constructor Parameters
205
+
206
+ | Parameter | Description |
207
+ | --- | --- |
208
+ | `flow_file` | Path to the pipeline JSON file (pipeline definition), defaults to `None`; pick it in the UI when omitted |
209
+ | `cfg_dir` | Modules directory holding `*.json` (struct / enum / typedef definitions), defaults to `None`; pick it in the UI when omitted |
210
+ | `parent` | Qt parent object, defaults to `None` |
211
+ | `elf_path` | Path to an ELF for layout verification, defaults to `None`; pick it in the UI when omitted |
212
+ | `appearance` | Button appearance overrides, `{key: {'mode': ..., 'icon': ..., 'text': ...}}` |
213
+ | `settings_org` | QSettings organization name, defaults to `'struct2ui'` |
214
+ | `settings_app` | QSettings application name, defaults to `'StructEditor'` |
215
+ | `toolbar_orientation` | Toolbar layout, `'horizontal'` (default) or `'vertical'` |
216
+ | `custom_button_position` | Where `add_custom_button()` drops buttons relative to the built-ins: `'end'` (after, default) or `'start'` (before / far left) |
217
+
218
+ ## Custom Toolbar Buttons
219
+
220
+ Host applications can add their own buttons to the top path bar via
221
+ `add_custom_button()`. All custom buttons form a single group, separated from
222
+ the built-in buttons by a divider that is added automatically — you only choose
223
+ which side they sit on (once, via the `custom_button_position` constructor arg).
224
+
225
+ ```python
226
+ editor = StructEditor("abc.json", "cfg_t", custom_button_position="end")
227
+
228
+ def on_send():
229
+ print(editor.current_values())
230
+
231
+ btn = editor.add_custom_button(
232
+ "Send", # tooltip / accessible label (shown when no icon)
233
+ on_send, # callable invoked on click (no arguments)
234
+ icon="icons/send.png", # optional; falls back to text when missing
235
+ checkable=False, # optional; make the button toggleable
236
+ )
237
+ # `btn` is the created QPushButton, returned so you can tweak it further.
238
+ ```
239
+
240
+ | Parameter | Description |
241
+ | --- | --- |
242
+ | `text` | Tooltip / accessible label, also shown when no icon is given. **Required.** |
243
+ | `on_click` | Callable invoked on click, takes no arguments. **Required.** |
244
+ | `icon` | Path to an icon file; falls back to `text` when the path is missing/empty. |
245
+ | `checkable` | Make the button toggleable, defaults to `False`. |
246
+
247
+ Returns the created `QPushButton`.
248
+
249
+ ## Programmatic API
250
+
251
+ Beyond the UI, `StructEditor` exposes methods to read state and drive exports
252
+ from code (e.g. wired to a custom button):
253
+
254
+ | Method | Description |
255
+ | --- | --- |
256
+ | `current_values() -> dict` | Live editor values of the active section, keyed by field name (empty dict when no parameter section is shown). |
257
+ | `current_paths() -> dict` | Current `{'modules', 'pipeline', 'elf'}` file paths; an empty string means that slot is unset. |
258
+ | `export_bin(path, show_dialogs=False) -> bool` | Write the current pipeline to a `.bin` at `path` — the programmatic equivalent of clicking Export → Binary. Returns `True` when there are no ELF layout errors (or no ELF is selected), `False` otherwise. |
259
+
260
+ `export_bin` verifies the ELF layout first when an ELF is selected (the file is
261
+ still written, matching the UI flow). With `show_dialogs=True` it pops the same
262
+ success / ELF-mismatch / failure dialogs the UI shows; with the default
263
+ `show_dialogs=False` no dialogs appear and render/write failures are raised as
264
+ exceptions for the caller to handle.
265
+
266
+ ```python
267
+ paths = editor.current_paths() # {'modules': ..., 'pipeline': ..., 'elf': ...}
268
+ name = paths['pipeline'] # derive an output name from the pipeline
269
+ ok = editor.export_bin("out/speech.bin") # True when ELF layout is clean
270
+ ```
271
+
272
+ ## Export API
273
+
274
+ The low-level export functions can be used standalone, without any UI:
275
+
276
+ ```python
277
+ from struct2ui.schema import SchemaRegistry
278
+ from struct2ui.exporters import (
279
+ emit_c, # sections + registry -> C source string
280
+ dumps_json, # -> JSON string
281
+ emit_bin, # -> binary bytes
282
+ merge_abi, # merge ABI info
283
+ verify_sections, # verify .bin layout against an ELF
284
+ parse_c_source, # C source -> parse result
285
+ build_schema_dict,
286
+ )
287
+ ```
288
+
289
+ ## Architecture
290
+
291
+ | Layer | Module | Responsibility |
292
+ | --- | --- | --- |
293
+ | Schema (pure data, no Qt) | `struct2ui.schema` | Parse `*.json` into a Field tree; spell-checking, semantic audits, pipeline cross-validation |
294
+ | UI rendering | `struct2ui.ui` | `WidgetFactory`, `FormRenderer`/`TreeRenderer`, array tables, `when` conditional binding |
295
+ | Export | `struct2ui.exporters` | C / JSON / bin export, C source reverse parsing, ELF verification |
296
+ | Top-level widget | `struct2ui.StructEditor` | Path bar / action buttons / content area; load report panel; QSettings path memory |
297
+
298
+ ## Development
299
+
300
+ ```bash
301
+ pip install -e ".[dev]" --no-build-isolation
302
+ python -m pytest
303
+ ```
304
+
305
+ Tests live under `tests/` and drive real Qt widgets headlessly on the offscreen platform.
306
+
307
+ ## License
308
+
309
+ [MIT](LICENSE) © Jay
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "struct2ui"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "Render C struct / JSON schema as editable PySide6 UI, export to C/JSON/bin"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,4 +1,4 @@
1
1
  from .editor import StructEditor
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.0"
4
4
  __all__ = ["StructEditor"]