Metadata-Version: 2.4
Name: struct2ui
Version: 0.4.3
Summary: Render C struct / JSON schema as editable PySide6 UI, export to C/JSON/bin
Author: Jay
License: MIT License
        
        Copyright (c) 2026 Jay
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Keywords: qt,pyside6,gui,c-struct,json-schema,code-generation,parameter-tuning
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: User Interfaces
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Qt.py>=1.3
Provides-Extra: pyside6
Requires-Dist: PySide6>=6.4; extra == "pyside6"
Provides-Extra: pyqt6
Requires-Dist: PyQt6>=6.4; extra == "pyqt6"
Provides-Extra: elf
Requires-Dist: pyelftools>=0.29; extra == "elf"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pyelftools>=0.29; extra == "dev"
Dynamic: license-file

# struct2ui

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.

## Features

- **JSON → UI**: describe C structs / enums / arrays with minimal JSON and auto-render Qt widgets (int→QSpinBox, float→QDoubleSpinBox, enum→QComboBox, etc.).
- **Embeddable**: `StructEditor` is a plain `QWidget` that drops into any PySide6 / PyQt UI.
- **Multi-format export**: edited results export to C source, JSON, or binary; C source can also be parsed back into a schema.
- **Validation**: keyword spell-checking (with “Did you mean X?” hints), semantic audits (`min<=max`, `step>0`), and pipeline cross-validation.
- **Qt-binding agnostic**: built on [Qt.py](https://github.com/mottosso/Qt.py) — works with PySide6 / PyQt6 / PySide2 / PyQt5.

## Installation

```bash
pip install struct2ui

# Pick a Qt binding (choose one)
pip install "struct2ui[pyside6]"
pip install "struct2ui[pyqt6]"

# Enable ELF layout verification (optional)
pip install "struct2ui[elf]"
```

> The library itself only depends on `Qt.py`; you must install a Qt binding (PySide6 / PyQt6 / etc.) yourself.
> ELF verification depends on `pyelftools`, installed via the `[elf]` extra.

## Quick Start

```python
from Qt import QtWidgets
from struct2ui import StructEditor

app = QtWidgets.QApplication([])

editor = StructEditor()
editor.set_sources(
    cfg_dir="cfg_t",        # modules dir: *.json holding struct/enum/typedef
    flow_file="abc.json",   # pipeline definition file
)
editor.resize(480, 600)
editor.show()

app.exec_()
```

## JSON Schema Reference

This is the core of the library: you describe your C types in JSON and the UI is
generated from it. Files live in the **modules directory** (`cfg_dir`), one
`*.json` per logical group. Each file is a flat object whose keys are either
**type names** (your structs / enums) or the reserved key `typedefs`.

### File layout

```jsonc
{
  // optional free-form metadata (ignored by the loader, allowed anywhere)
  "version": "1.0",
  "description": "audio EQ parameters",

  // type aliases: map a custom C type to a primitive
  "typedefs": { "gain_t": "int32_t", "freq_t": "float" },

  // a struct definition
  "eq_cfg_t": {
    "type": "struct",
    "items": [
      { "name": "enabled", "type": "uint8_t", "value": 1 },
      { "name": "gain",    "type": "gain_t",  "value": 0,
        "min": -12, "max": 12, "step": 1, "unit": "dB",
        "tip": "output gain", "when": { "enabled": 1 } }
    ]
  },

  // an enum definition
  "mode_t": {
    "type": "enum",
    "items": { "OFF": 0, "LOW": 1, "HIGH": 2 }
  }
}
```

### Top-level blocks

| Block type | Keys | Notes |
| --- | --- | --- |
| `struct` | `type`, `items` (list of field specs) | `items` is required |
| `enum` | `type`, `items` (object `{NAME: value}`) | reads back the enumerator name |
| `typedefs` | object `{alias: real_type}` | resolved before field building |

Free-form metadata keys are allowed and ignored at any level: `version`,
`description`, `author`, `comment`, `note`.

### Field properties (inside a struct's `items`)

Each entry in `items` is a **field spec**. Only the following keys are
recognized — any other key triggers an "unknown keyword" error (with a
"Did you mean …?" hint):

| Property | Applies to | Description |
| --- | --- | --- |
| `name` | all | **Required.** C field name. |
| `type` | all | **Required.** C type or a `typedefs` alias (e.g. `int32_t`, `float`, `char`, an enum/struct name). |
| `value` | all | Default value. For arrays, a list (per element) or a scalar (applied to every element). |
| `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). |
| `min` / `max` | int, float | Numeric bounds. Validated `min <= max`. Drive spin-box / slider / dial ranges. |
| `step` | int, float | Increment, must be `> 0`. |
| `decimals` | float | Number of fractional digits shown/stored. Non-negative integer; defaults to digits implied by `step`. |
| `unit` | all | Unit suffix shown in the label, e.g. `gain (dB)`. |
| `tip` | all | Tooltip text. |
| `choices` | int, float, `char[N]` | Discrete value set rendered as a combo (see below). |
| `when` | all | Conditional enable/disable (see below). |
| `widget` | all | Override the auto-picked editor (see table below). |

### `widget` values

When omitted, the widget is inferred from `type` (and from `min/max`,
`choices`). Override it explicitly with `widget`:

| `widget` | Valid for | Renders as |
| --- | --- | --- |
| `checkbox` | int | Check box (also auto-picked when `min:0, max:1`). |
| `toggle` | bool/int | Toggle push-button. |
| `combo` | int, float, `char[N]` | Combo box; pair with `choices`. |
| `slider` | int | Slider with min/max labels (requires `min` & `max`). |
| `dial` | int | Rotary dial popup (requires `min` & `max`). |
| `file` | array | Path label + Browse button; loads array values from a text file. |
| `multiline` | array | Single-line shell that pops up a multi-line editor. |
| `table` | struct array | Force grid layout (one column per struct member). |

Numeric fields without an explicit `widget` become a `QSpinBox` /
`QDoubleSpinBox`; scalar arrays become a comma-separated line edit.

### `choices` (discrete values)

A non-enum integer / float / `char[N]` field can be constrained to a fixed set.
Two forms are accepted:

```jsonc
"choices": [0, 1, 2, 3]                              // label = str(value)
"choices": [{ "Off": 0 }, { "Low": 1 }, { "High": 2 }]  // explicit labels
```

The combo **reads back the underlying value** (not the label), so C / JSON / bin
export still emits the number (or string for `char[N]`). Audits enforce: each
value within `[min, max]` when declared, and `value` (the default) must be one
of the choices.

### `when` (conditional enable/disable)

A field can be greyed out unless other fields hold specific values. `when` is an
object mapping a **sibling field name** to its required value; all entries must
match (logical AND):

```jsonc
{ "name": "cutoff", "type": "float", "when": { "enabled": 1, "mode": "HIGH" } }
```

The dependency name is matched against the dotted leaf name in scope. When a
dependency is not found, the field stays enabled.

### `count` expressions & multi-dimensional arrays

`count` may be an integer, a `#define` constant name, or an integer expression
of those using `+ - * / % ()` (e.g. `MAX_BAND_NUM - 1`). A list makes a
multi-dimensional array that is **stored flat** to match the C ABI:

```jsonc
{ "name": "matrix", "type": "float", "count": [3, 4] }  // 12 contiguous floats, shown as [3][4]
```

`char[N]` is treated as a C string by default (use `widget` to override).

### Validation

Loading runs three layers of checks, surfaced in the **load report** panel:

- **Keyword spelling** — unknown keys report "Did you mean `tip`?" style hints.
- **Semantic audits** — `min <= max`, `step > 0`, `count > 0`, choices within
  bounds, default is one of the choices.
- **Pipeline cross-validation** — values in the pipeline file are checked
  against the schema (range, choices, enum membership).

## Embedding into an Existing UI

`StructEditor` is a regular `QWidget`; just put it into a layout:

```python
from Qt import QtWidgets
from struct2ui import StructEditor

class MyWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        editor = StructEditor()
        editor.set_sources(cfg_dir="cfg_t", flow_file="abc.json")
        self.setCentralWidget(editor)
```

`StructEditor` takes no source paths at construction time and keeps no
persistent state of its own. Point it at its sources after creating it with
`set_sources()` (or let the user pick them at runtime via Settings). If you
want to remember the last-used paths across runs, store them in the host app
and call `set_sources()` again at startup.

### Constructor Parameters

| Parameter | Description |
| --- | --- |
| `parent` | Qt parent object, defaults to `None` |
| `appearance` | Button appearance overrides, `{key: {'mode': ..., 'icon': ..., 'text': ...}}` |
| `custom_button_position` | Where `add_custom_button()` drops buttons relative to the built-ins: `'end'` (after, default) or `'start'` (before / far left) |

### Setting Sources

Source paths are configured after construction, not passed to the constructor.
Omitting an argument leaves that slot untouched.

```python
editor.set_sources(
    cfg_dir="cfg_t",      # modules dir: *.json with struct/enum/typedef defs
    flow_file="abc.json", # pipeline definition file
    elf_file="fw.elf",    # optional ELF for .bin layout verification
)
```

| `set_sources` argument | Description |
| --- | --- |
| `cfg_dir` | Modules directory holding `*.json` (struct / enum / typedef definitions) |
| `flow_file` | Path to the pipeline JSON file (pipeline definition) |
| `elf_file` | Path to an ELF for `.bin` layout verification; setting it enables verification (toggle later via the Settings checkbox, which keeps the path) |
| `elf_enabled` | Override whether ELF verification runs, independent of whether a path is set |
| `reload` | Rebuild schema / flow / UI from the new sources, defaults to `True` |
| `async_load` | Run that reload on a background thread (defaults to `False`). See [Async Loading](#async-loading) |

Toolbar orientation is set with `set_flow_orientation('horizontal' | 'vertical')`.

#### Async Loading

Parsing the modules, validating the pipeline and (optionally) reading the ELF
all happen during the reload. For large module sets or ELFs this can take a
while, and by default it runs synchronously — `set_sources()` blocks until the
editor is fully built, which is what programmatic callers and tests want.

A GUI host can avoid the startup freeze by passing `async_load=True` *after*
`show()`: the window appears immediately and shows a `Loading...` placeholder
while the heavy parsing runs on a worker thread, then swaps in the real content
when it is ready. Reloads triggered while one is already running supersede the
older one.

```python
editor.show()
editor.set_sources(
    cfg_dir="cfg_t",
    flow_file="abc.json",
    elf_file="fw.elf",
    async_load=True,        # parse off the GUI thread; window stays responsive
)
```

## Custom Toolbar Buttons

Host applications can add their own buttons to the top path bar via
`add_custom_button()`. All custom buttons form a single group, separated from
the built-in buttons by a divider that is added automatically — you only choose
which side they sit on (once, via the `custom_button_position` constructor arg).

```python
editor = StructEditor(custom_button_position="end")
editor.set_sources(cfg_dir="cfg_t", flow_file="abc.json")

def on_send():
    print(editor.current_values())

btn = editor.add_custom_button(
    "Send",                 # tooltip / accessible label (shown when no icon)
    on_send,                # callable invoked on click (no arguments)
    icon="icons/send.png",  # optional; falls back to text when missing
    checkable=False,        # optional; make the button toggleable
)
# `btn` is the created QPushButton, returned so you can tweak it further.
```

| Parameter | Description |
| --- | --- |
| `text` | Tooltip / accessible label, also shown when no icon is given. **Required.** |
| `on_click` | Callable invoked on click, takes no arguments. **Required.** |
| `icon` | Path to an icon file; falls back to `text` when the path is missing/empty. |
| `checkable` | Make the button toggleable, defaults to `False`. |

Returns the created `QPushButton`.

## Programmatic API

Beyond the UI, `StructEditor` exposes methods to read state and drive exports
from code (e.g. wired to a custom button):

| Method | Description |
| --- | --- |
| `set_sources(cfg_dir=None, flow_file=None, elf_file=None, elf_enabled=None, reload=True, async_load=False)` | Point the editor at its source files after construction. See [Setting Sources](#setting-sources). |
| `set_flow_orientation(orientation)` | Switch the flow-button strip between `'horizontal'` (top, default) and `'vertical'` (left). Rebuilds the strip and keeps the active stage selected. |
| `show_section(label) -> None` | Open the stage/section named `label` in the content area (the same as clicking its flow button). Paints the error panel instead when that stage has pipeline errors; clears the content when the label is unknown. |
| `current_values() -> dict` | Live editor values of the active section, keyed by field name (empty dict when no parameter section is shown). |
| `current_paths() -> dict` | Current `{'modules', 'pipeline', 'elf'}` file paths; an empty string means that slot is unset. |
| `add_custom_button(text, on_click, icon=None, checkable=False) -> QPushButton` | Add a host button to the top path bar. See [Custom Toolbar Buttons](#custom-toolbar-buttons). |
| `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 ELF verification is off), `False` otherwise. |

`export_bin` verifies the ELF layout first when an ELF is selected and enabled (the file is
still written, matching the UI flow). With `show_dialogs=True` it pops the same
success / ELF-mismatch / failure dialogs the UI shows; with the default
`show_dialogs=False` no dialogs appear and render/write failures are raised as
exceptions for the caller to handle.

```python
paths = editor.current_paths()           # {'modules': ..., 'pipeline': ..., 'elf': ...}
name = paths['pipeline']                  # derive an output name from the pipeline
ok = editor.export_bin("out/speech.bin")  # True when ELF layout is clean
```

## Export API

The low-level export functions can be used standalone, without any UI:

```python
from struct2ui.schema import SchemaRegistry
from struct2ui.exporters import (
    emit_c,          # sections + registry -> C source string
    dumps_json,      # -> JSON string
    emit_bin,        # -> binary bytes
    merge_abi,       # merge ABI info
    verify_sections, # verify .bin layout against an ELF
    parse_c_source,  # C source -> parse result
    build_schema_dict,
)
```

## Architecture

| Layer | Module | Responsibility |
| --- | --- | --- |
| Schema (pure data, no Qt) | `struct2ui.schema` | Parse `*.json` into a Field tree; spell-checking, semantic audits, pipeline cross-validation |
| UI rendering | `struct2ui.ui` | `WidgetFactory`, `FormRenderer`/`TreeRenderer`, array tables, `when` conditional binding |
| Export | `struct2ui.exporters` | C / JSON / bin export, C source reverse parsing, ELF verification |
| Top-level widget | `struct2ui.StructEditor` | Action toolbar / content area; Settings panel for source paths; load report panel |

## Development

```bash
pip install -e ".[dev]" --no-build-isolation
python -m pytest
```

Tests live under `tests/` and drive real Qt widgets headlessly on the offscreen platform.

## License

[MIT](LICENSE) © Jay
