{"id":52091292,"url":"https://github.com/aether-lang-dev/aether-ui","last_synced_at":"2026-08-04T06:01:28.508Z","repository":{"id":359203774,"uuid":"1234434980","full_name":"aether-lang-dev/aether-ui","owner":"aether-lang-dev","description":"Pseudo-declarative UI library for Aether","archived":false,"fork":false,"pushed_at":"2026-07-31T20:14:38.000Z","size":50563,"stargazers_count":1,"open_issues_count":18,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-31T20:22:02.425Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aether-lang-dev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null,"disclosure":null}},"created_at":"2026-05-10T07:18:35.000Z","updated_at":"2026-07-31T20:14:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aether-lang-dev/aether-ui","commit_stats":null,"previous_names":["aether-lang-org/aether-ui","aether-lang-dev/aether-ui"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aether-lang-dev/aether-ui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aether-lang-dev%2Faether-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aether-lang-dev%2Faether-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aether-lang-dev%2Faether-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aether-lang-dev%2Faether-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aether-lang-dev","download_url":"https://codeload.github.com/aether-lang-dev/aether-ui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aether-lang-dev%2Faether-ui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36263568,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-08-04T02:00:06.901Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-08-04T06:01:27.614Z","updated_at":"2026-08-04T06:01:28.492Z","avatar_url":"https://github.com/aether-lang-dev.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Aether UI for Aether\n\nPort of the [Perry](https://github.com/PerryTS/perry) UI framework to Aether.\nDeclarative widget DSL backed by GTK4 (Linux and FreeBSD), AppKit (macOS),\nand a native Win32 backend (Windows) — the three backend implementations\nshare the same ABI declared in `aether_ui_backend.h`. Uses Aether's\ntrailing-block builder pattern.\n\n## Credits\n\nThis module is a from-scratch Aether + C rewrite of the aether-ui Rust crates\nfrom the [Perry project](https://github.com/PerryTS/perry) by the Perry\ncontributors. The Rust implementations (`aether-ui-gtk4`, `aether-ui-macos`, and\nthe core `aether-ui` crate) were used as reference for architecture, widget\nAPI design, reactive state bindings, and platform-specific GTK4/AppKit\npatterns. Based on commit\n[`7f1e3f9`](https://github.com/PerryTS/perry/commit/7f1e3f979832c33d2da79970ea62bc1b74c2e31a)\nof the `main` branch.\n\nPortions Copyright (c) 2026 Perry Contributors collectively, and portions Copyright (c) 2026 Aether Contributors collectively. MIT License.\n\n### Apps built on other people's ideas\n\nSome apps under `apps/` port or borrow from existing projects. Each\ncarries its own `NOTICE` (and the upstream licence verbatim) beside its\nsource; the summary:\n\n- **Maerkdown** (`apps/maerkdown`) — the word-as-widget markdown editor.\n  Its extended inline syntax (`++insertion++`, `||spoiler||`,\n  `==highlight==`, `^superscript^`, `~subscript~`) is taken from the\n  [Extended Markdown Syntax](https://github.com/kotaindah55/extended-markdown-syntax)\n  plugin for Obsidian by Kotaindah55 (Sheva Ihza),\n  [MIT](https://github.com/kotaindah55/extended-markdown-syntax/blob/main/LICENSE).\n  The delimiters and their meanings come from that project's documented\n  rules; no code was copied, and the parser is an independent\n  implementation over this editor's own document model.\n- **Font Picker** (`apps/font_picker`) — a rule-for-rule port of\n  [Javascript Font Picker](https://www.jsfontpicker.com/), MIT, portions\n  Copyright (c) 2024-2025 Zygomatic.\n- **Falling Blocks** (`apps/falling_blocks`) — derived from\n  [fallingblocks](https://github.com/SanderKlootwijk/fallingblocks) and\n  therefore **GPL-3.0**, unlike the rest of this repository. See the\n  header in that app's source before distributing binaries built from it.\n\n## Quick start\n\n### Linux (GTK4)\n\n```bash\nsudo apt install libgtk-4-dev   # Debian/Ubuntu\n./build.sh example_counter.ae build/counter\n./build/counter\n```\n\n### FreeBSD / GhostBSD (GTK4)\n\nSame GTK4 backend as Linux; `build.sh` detects FreeBSD and uses clang.\n\n```bash\nsudo pkg install gtk4 pkgconf   # ensure a zlib.pc exists for freetype2 -\u003e zlib\n./build.sh example_counter.ae build/counter\n./build/counter\n```\n\n`tests/spec_matrix.sh` needs no display of its own — on FreeBSD it starts a\nprivate Xvfb (`pkg install xorg-vfbserver`), unprivileged, and any pre-set\n`$DISPLAY` is respected instead.\n\n### macOS (AppKit)\n\n```bash\n./build.sh example_counter.ae build/counter\n./build/counter\n```\n\n### Windows (native Win32)\n\nBuild from an MSYS2 MinGW64 shell (no extra dev libraries — USER32, GDI+\nand Common Controls ship with Windows itself):\n\n```bash\n./build.sh example_counter.ae build/counter\n./build/counter.exe\n```\n\nBackend-level smoke tests (headless, no display needed) for any platform:\n\n```bash\n# widget + driver tests for current backend\ngcc tests/test_widgets.c aether_ui_gtk4.c -o build/test_widgets $(pkg-config --cflags --libs gtk4) -I. -Itests -lpthread -lm \u0026\u0026 ./build/test_widgets    # widget + driver tests for current backend\n# microbenchmarks, CSV to stdout        # microbenchmarks, CSV to stdout\n```\n\nSee [docs/aether-ui-windows.md](docs/aether-ui-windows.md) for\nWindows-specific details (DPI model, dark mode, widget mappings, known\nlimitations).\n\n## DSL with Scope\n\nAether UI is a **\"DSL with Scope\"** — Matz's own name (he coined it when\nasked to name the pattern) for the builder-block style: nested blocks that\ndescribe structure declaratively while keeping full imperative power, with an\n*implicit receiver* so children wire to their parent without explicit\nplumbing. It runs in the Smalltalk-blocks / Ruby-Shoes / Groovy-SwingBuilder /\nKotlin-Compose / SwiftUI lineage — and, unlike a markup format, the blocks are\n**executed code**, not parsed into a DOM for some later actioning. See\n[Paul Hammant's \"That Ruby and Groovy Language Feature\"](https://paulhammant.com/2024/02/14/that-ruby-and-groovy-language-feature.html)\nfor the full tour, and Aether's own\n[`docs/closures-and-builder-dsl.md`](https://github.com/aether-lang-org/aether/blob/main/docs/closures-and-builder-dsl.md)\nfor the mechanism (trailing blocks, the `_ctx` implicit-receiver convention,\nand `builder … with` \"configure then execute\").\n\nA UI is opened inside a **surface** scope. The surface's *kind* decides its\nlifecycle (see [Surfaces](#surfaces-window--render_to--record) below):\n\n```aether\nimport ui\n\nmain() {\n    counter = aether_ui.ui_state(0)\n\n    aether_ui.window(\"My App\", 400, 200) {\n        aether_ui.root_vstack(10) {\n            aether_ui.text(\"Hello World\")\n            aether_ui.text_bound(counter, \"Count: \", \"\")\n            aether_ui.hstack(5) {\n                aether_ui.button(\"+1\") callback {\n                    aether_ui.ui_set(counter, aether_ui.ui_get(counter) + 1)\n                }\n                aether_ui.button(\"-1\") callback {\n                    aether_ui.ui_set(counter, aether_ui.ui_get(counter) - 1)\n                }\n            }\n        }\n    }\n}\n```\n\nThe `window(…) { … }` block builds the tree, then — because it's a `builder`\nfunction whose body runs *after* the block — opens the window and runs the\nevent loop. No trailing `app_run(root)`: the surface *is* the entry point.\n\n## Surfaces (window / render_to / record)\n\nA **surface** is the ambient destination a widget/drawing block populates.\nThe kind decides lifecycle:\n\n| Surface | Lifecycle | What it is |\n|---------|-----------|------------|\n| `window(title, w, h) { … }` | **lived** — runs the event loop, ends on window close | An on-screen interactive window. Absorbs the old `app_run`. |\n| `render_to(target, w, h) { … }` | **bounded** — one render pass, returns | Draw into a target: pixel buffer, PNG, PDF, **paper**. No event loop. |\n| `record(w, h) { … }` | **bounded** — captures, returns | A test/recording surface — inspect what was built. No event loop. |\n| `window_run(title, w, h, root)` | lived | Explicit-root variant of `window` for trees built imperatively (e.g. a `root_grid` whose cells are `grid_place`'d in). |\n\nInteractive verbs (`onclick`, `onhover`) used inside a **bounded** surface are\n*diagnostic-inert*: they render but the handler never fires (there's no event\nloop to deliver to). The diagnostic is **collected on the surface** by default\n(read it with `surface_diagnostics(handle)`); routing it to stderr or a hard\nfail is an explicit opt-in, never the default — the framework never writes to\na stream you didn't ask it to.\n\n**Inside a surface block, use the context-attaching layout verbs** (`vstack`,\n`hstack`, `zstack`, …) — **not** the `root_*` variants (`root_vstack`,\n`root_hstack`). The `root_*` verbs are *detached*: they take no builder context\nand so don't attach to the enclosing surface, leaving you with a window that\nmaps but renders blank. The `root_*` forms exist only for the explicit-root\n`window_run(title, w, h, root)` path, where you build the tree imperatively and\nhand the root in. Inside `window {…}` / `render_to {…}` / `record {…}`, always\n`vstack` (which the compiler auto-parents to the surface via the `_ctx`\nconvention).\n\nWhy three verbs instead of one `app_run`? Because `app_run` welded together\nthree jobs — create the window, mount the tree, run the loop — and forced that\n*lived* shape onto every program. Most surfaces aren't lived: a render-to-PNG,\na print-to-paper, a headless test needs no loop and ends by reaching `}`. Only\na live window has \"a life of its own\" that ends on an external event, so only\n`window` carries the loop.\n\n## Widgets available\n\n| Widget      | Aether function                                       | GTK4               | AppKit                  | Win32                      |\n|-------------|-------------------------------------------------------|--------------------|-------------------------|----------------------------|\n| Text        | `ui.text(\"label\")`                             | GtkLabel           | NSTextField (label)     | STATIC                     |\n| Button      | `ui.button(\"label\") callback { }`              | GtkButton          | NSButton                | BUTTON (BS_PUSHBUTTON)     |\n| VStack      | `ui.vstack(spacing) { children }`              | GtkBox vertical    | NSStackView vertical    | AetherUIStack (custom)     |\n| HStack      | `ui.hstack(spacing) { children }`              | GtkBox horizontal  | NSStackView horizontal  | AetherUIStack (custom)     |\n| Spacer      | `ui.spacer()`                                  | Expanding GtkBox   | NSView flex filler      | flex placeholder           |\n| Divider     | `ui.divider()`                                 | GtkSeparator       | NSBox separator         | GDI line (custom class)    |\n| TextField   | `ui.textfield(\"hint\") callback \\|val\\| { }`    | GtkEntry           | NSTextField             | EDIT                       |\n| SecureField | `ui.securefield(\"hint\") callback \\|val\\| { }`  | GtkPasswordEntry   | NSSecureTextField       | EDIT (ES_PASSWORD)         |\n| Toggle      | `ui.toggle(\"label\") callback \\|active\\| { }`   | GtkCheckButton     | NSButton (switch)       | BUTTON (BS_AUTOCHECKBOX)   |\n| Slider      | `ui.slider(min, max, init) callback \\|val\\|`   | GtkScale           | NSSlider                | TRACKBAR (comctl32)        |\n| Picker      | `ui.picker() callback \\|idx\\| { }`             | GtkDropDown        | NSPopUpButton           | COMBOBOX (CBS_DROPDOWNLIST)|\n| TextArea    | `ui.textarea(\"hint\") callback \\|val\\| { }`     | GtkTextView        | NSTextView              | EDIT (ES_MULTILINE)        |\n| ProgressBar | `ui.progressbar(0.75)`                         | GtkProgressBar     | NSProgressIndicator     | PROGRESS (comctl32)        |\n| ScrollView  | `ui.scrollview() { children }`                 | GtkScrolledWindow  | NSScrollView            | AetherUIStack + WS_VSCROLL |\n| Grid        | `ui.root_grid(cols, rspace, cspace)` + `grid_place(...)` | GtkGrid   | NSGridView              | AetherUIGrid (custom)      |\n| Menu bar    | `ui.menu_bar()` + `menu()` + `menu_item()`     | GMenu / GActionMap | NSMenu                  | HMENU (CreateMenu/SetMenu) |\n\n## Reactive state\n\n```aether\ncounter = aether_ui.ui_state(0)              // create state cell\naether_ui.text_bound(counter, \"Val: \", \"\")   // auto-updating text\naether_ui.ui_set(counter, 42)                // triggers re-render\nval = aether_ui.ui_get(counter)              // read current value\n```\n\n## Widget accessors\n\n```aether\naether_ui.set_text(handle, \"new text\")       // set textfield value\ntext = aether_ui.get_text(handle)            // get textfield value\naether_ui.set_toggle(handle, 1)              // set toggle on/off\naether_ui.set_slider(handle, 75.0)           // set slider position\naether_ui.set_progress(handle, 0.5)          // set progress bar\n```\n\n## Examples\n\n| Example | Widgets demonstrated |\n|---------|---------------------|\n| `example_counter.ae` | text, button, hstack, vstack, spacer, divider, reactive state |\n| `example_form.ae` | textfield, securefield, toggle, slider, textarea, progressbar |\n| `example_picker.ae` | picker (dropdown), picker_add |\n| `example_styled.ae` | form, section, zstack, bg_color, bg_gradient, font_size, corner_radius |\n| `example_system.ae` | alert, clipboard, dark mode detection, sheet |\n| `example_canvas.ae` | canvas drawing, fill_rect, stroke, on_hover, on_double_click |\n| `example_testable.ae` | AetherUIDriver test server, sealed widgets, remote control banner |\n\n## AetherUIDriver — automated UI testing, baked in\n\nAether UI ships with a built-in HTTP test server that lets any language\nwith an HTTP client drive the app:\n\n```aether\naether_ui.enable_test_server(9222, root)\n```\n\nOr set `AETHER_UI_TEST_PORT=9222` in the environment before launching —\nno code changes needed. A red \"Under Remote Control\" banner is injected\nso a user can't mistake a test-driven session for a real one.\n\nThe HTTP API exposes `/widgets` (list + filter), `/widget/{id}` (state),\n`/widget/{id}/click | set_text | toggle | set_value` (mutations), and\n`/state/{id}` + `/state/{id}/set` (reactive-state cells). See the full\nreference and end-to-end examples in\n**[docs/aether-ui-testing.md](../../docs/aether-ui-testing.md)** —\nincluding Bash, Python, and JavaScript test-suite skeletons plus the\n`AETHER_UI_HEADLESS=1` flag for unattended CI.\n\nFor most native UI frameworks you have to bolt on Selenium/Appium. With\naether_ui it's part of the framework and works identically on macOS,\nLinux, FreeBSD, and Windows via the shared\n[`aether_ui_test_server.c`](aether_ui_test_server.c).\n\n### Widget sealing\n\nMark widgets as non-automatable — the test server returns 403 for sealed widgets:\n\n```aether\ndanger = aether_ui.button(\"Delete Everything\") callback { ... }\naether_ui.seal_widget(danger)\n```\n\nThis maps to Aether's `hide`/`seal` philosophy: the app author declares which\ncapabilities the test harness is denied, not the other way around.\n\n## Architecture\n\n| Layer | File | Role |\n|-------|------|------|\n| Aether DSL | `ui/module.ae` | Builder-pattern wrappers with `_ctx` auto-injection; surface verbs (`window`/`render_to`/`record`) |\n| GTK4 backend | `aether_ui_gtk4.c` | Linux + FreeBSD: GTK4 C API calls, Cairo canvas, test server |\n| macOS backend | `aether_ui_macos.m` | macOS: AppKit Objective-C |\n| Win32 backend | `aether_ui_win32.c` | Windows: USER32 + GDI+ + Common Controls |\n| C header | `aether_ui_backend.h` | Shared backend ABI — implemented by all three backends (four platforms; FreeBSD shares GTK4) |\n| Build script | `build.sh` | Auto-detects platform (Darwin/Linux/FreeBSD/MinGW) |\n| Test script | `test_automation.sh` | Example curl-based test suite (17 assertions) |\n| Widget tests | `tests/test_widgets.c` | Cross-platform C-level smoke suite (40 assertions) |\n| Driver tests | `tests/test_driver.sh` | HTTP integration against the embedded test server |\n| Benchmarks | `benchmarks/bench_widgets.c` | CSV microbenchmarks — widget create, layout, state, canvas |\n\n## Platform support\n\n| Platform | Backend                         | Status                                                                             |\n|----------|---------------------------------|------------------------------------------------------------------------------------|\n| Linux    | GTK4  (`aether_ui_gtk4.c`)      | Full — all widgets, canvas, events, styling, AetherUIDriver test server            |\n| macOS    | AppKit (`aether_ui_macos.m`)    | Full — all widgets, canvas, events, styling, AetherUIDriver test server            |\n| Windows  | Native Win32 (`aether_ui_win32.c`) | Full — USER32 + GDI+ + Common Controls; per-monitor DPI v2; immersive dark mode; AetherUIDriver via winsock2 |\n| FreeBSD  | GTK4  (`aether_ui_gtk4.c`)      | Full — shares the Linux backend; clang build, private-Xvfb spec runs           |\n\n\"Full\" above means the backend implements the whole widget/canvas/event/\nstyling surface plus AetherUIDriver — not that every suite is green on every\nbox. `tests/spec_matrix.sh` is the authority; run it on the platform you care\nabout. Most recent full runs: Linux 228/0, FreeBSD 223/0 (only `lismusic`,\nwhich needs the sqlite contrib archive installed on that host).\n\n## Status\n\nAll groups (1–7) plus AetherUIDriver are implemented on every backend.\n`# widget + driver tests for current backend\ngcc tests/test_widgets.c aether_ui_gtk4.c -o build/test_widgets $(pkg-config --cflags --libs gtk4) -I. -Itests -lpthread -lm \u0026\u0026 ./build/test_widgets` runs the cross-platform smoke suite and, on\nWindows, the HTTP driver integration. `# microbenchmarks, CSV to stdout` prints a\nCSV of per-operation latencies.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faether-lang-dev%2Faether-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faether-lang-dev%2Faether-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faether-lang-dev%2Faether-ui/lists"}