
Graphical HAL API navigator and navigation library — follow links, expand URI templates, inspect embedded resources and craft requests; extensible plugin system, test DSL, and JS/Wasm/C facades.
= NaHAL — HAL navigator :toc: left :toclevels: 3 :icons: font :source-highlighter: highlight.js
image:https://img.shields.io/badge/demo-live-2b6cb0[Live demo,link=https://c06a.github.io/NaHAL/] image:https://img.shields.io/maven-central/v/com.helpchoice.nahal/nahal-ui[Maven Central,link=https://central.sonatype.com/artifact/com.helpchoice.nahal/nahal-ui] image:https://img.shields.io/github/v/release/C06A/NaHAL[Latest release,link=https://github.com/C06A/NaHAL/releases/latest] image:https://img.shields.io/badge/license-Apache%202.0-blue[License,link=https://www.apache.org/licenses/LICENSE-2.0]
NaHAL is a graphical navigator for https://stateless.group/hal_specification.html[HAL (Hypertext Application Language)] APIs, plus the navigation layer it is built on, for https://kotlinlang.org/docs/multiplatform.html[Kotlin Multiplatform]. Point it at a HAL endpoint and walk the API by following links, expanding URI templates, inspecting embedded resources and building requests — on the desktop, on a phone, in a browser, or embedded in your own application.
The GUI offers two switchable layouts: panes (traversal rail, accordion resource viewer, request builder) and graph (a node-link canvas of the traversal with a detail drawer).
The HAL client library and the example plugins are not part of this repository.
com.helpchoice.nahal:haldish. Change the HAL client there, not here.:core, :ui, :androidApp, :testkit,
:testkit-groovy, plus the iosApp/ Xcode host.== What is in this repository
|=== | Module | Artifact | Role
| :core
| not published — its sources compile into nahal-ui and haldish-testkit
| Navigation layer over Haldish — HalNavigator, LinkSelector, DocLinkResolver, config-driven plugin loading, and JS / Wasm / C facades. No application entry point. Still builds the nahal-core shared library and C header shipped with the GitHub release.
| :ui
| com.helpchoice.nahal:nahal-ui
| The Compose Multiplatform navigator GUI: desktop (JVM and native macOS), browser (JS and Wasm), Android library variant, iOS NahalUI.framework. Library only — every platform's entry point calls NaHalNavigator().
| :androidApp
| not published — released as an APK/AAB
| The installable Android app: one Activity hosting NaHalNavigator().
| :testkit
| com.helpchoice.nahal:haldish-testkit
| Kotlin test DSL for driving HAL APIs in tests.
| :testkit-groovy
| com.helpchoice.nahal:haldish-testkit-groovy
| Groovy/Spock bindings for the testkit.
| iosApp/
| not a Gradle module
| Xcode project that links NahalUI.framework (produced by :ui) into an iOS app.
|===
== Supported platforms
|=== | Platform | What you get | How it is delivered
| macOS (JVM)
| Desktop app
| .dmg installer, or ./gradlew :ui:jvmRun
| macOS (native)
| Desktop app, no JVM required
| nahal-app-macos-{arm64,x64}-<version>.zip, or ./gradlew :ui:bundleMacosArm64App / :ui:bundleMacosX64App
| Linux
| Desktop app
| .deb installer, or ./gradlew :ui:jvmRun
| Windows
| Desktop app
| .msi installer, or ./gradlew :ui:jvmRun
| Browser (JS)
| Web app
| nahal-ui-web-<version>.zip, served from any static host
| Browser (Wasm)
| Web app, WasmGC browsers only
| nahal-ui-wasm-<version>.zip; see <<wasm-status,WebAssembly build>>
| Android
| Installable app (API 24+)
| nahal-android-<version>.apk, or ./gradlew :androidApp:assembleRelease
| iOS
| App, and an embeddable view controller
| nahal-app-ios-simulator-<version>.zip (unsigned Apple-silicon simulator build), or Xcode over NahalUI.framework; MainViewController() from the nahal-ui klib to embed it
| Linux / macOS / Windows (C)
| Shared library
| libnahal_core.{so,dylib,dll} + generated header
|===
== Running the app
=== Desktop, any OS (JVM)
Installers are built by Compose Desktop, which only produces the host format — a full set needs a CI matrix across the three operating systems:
./gradlew :ui:packageDistributionForCurrentOS
These installers embed a Java runtime, so the end user installs no JVM. Linux and Windows get one of them rather than a JVM-free native build, because Compose Multiplatform ships no Kotlin/Native renderer for either — macOS is the only desktop OS where the native path below exists.
=== macOS, without a JVM (Kotlin/Native)
The macOS targets also build a standalone executable and wrap it in an app bundle:
./gradlew :ui:runMacosArm64App # Apple silicon — builds NaHAL.app and opens it ./gradlew :ui:runMacosX64App # Intel
runMacos*App launches through open, and macOS LaunchServices does not pass the shell
environment to the app. Environment variables such as HALDISH_CONFIG set in a terminal will not
reach it — run the executable directly (ui/build/bin/macosArm64/releaseExecutable/ui.kexe) when
you need them.=== Browser
Nothing to install: the latest release is hosted at
https://c06a.github.io/NaHAL/[c06a.github.io/NaHAL] — a landing page linking the JS build (js/)
and the WebAssembly one (wasm/). Being served over HTTPS, that copy can only reach APIs that are
HTTPS and CORS-enabled for the origin, so for a HAL server on your own machine or network take
the bundle below and serve it from http://localhost.
Build the bundle, or take the released zip:
./gradlew :ui:jsBrowserDistribution
unzip nahal-ui-web-.zip -d nahal-web cd nahal-web python3 -m http.server 8080
The bundle contains index.html, the compiled ui.js, the Skia runtime (skiko.js, skiko.wasm) and composeResources/. Three things to know:
file:// fails, because skiko.js fetches skiko.wasm.application/wasm for .wasm (Python's http.server and modern nginx already do; older nginx needs types { application/wasm wasm; }).index.html are relative, so hosting under a sub-path works unchanged.Because a browser build is subject to the same-origin policy, every API you navigate to must return
Access-Control-Allow-Origin for your host. APIs that work in the desktop build can still fail in
the browser for this reason alone.
[[wasm-status]] ==== WebAssembly build
The same UI also compiles to WebAssembly, shipped as its own asset next to the JS one:
./gradlew :ui:wasmJsBrowserDistribution
It serves under the same rules as the JS bundle — HTTP only, application/wasm for .wasm,
relative paths — with two differences:
skiko.js bootstrap. Compose's Wasm runtime loads Skia itself, so index.html simply
loads ui.js; the Skia and application code arrive as two .wasm blobs.For 2.1.0 the Wasm zip is the smaller download — 4.3 MB against the JS bundle's 5.6 MB — while also keeping the draw path out of a JS interpreter. Browser support, not size, is what keeps the JS bundle in the release.
=== Android
:androidApp is the installable app; :ui carries the reusable android library variant that ships
inside nahal-ui. Minimum API level 24, compiled against API 35.
Release signing comes from NAHAL_KEYSTORE, NAHAL_KEYSTORE_PASSWORD, NAHAL_KEY_ALIAS and
NAHAL_KEY_PASSWORD (Gradle properties or environment variables). Without them the release build
falls back to the local debug key — installable from a GitHub download, not acceptable for a Play
Store upload.
=== iOS
Gradle links NahalUI.framework; Xcode turns it into the app. Building needs the iOS 17 SDK
(Xcode 15+), because Compose Multiplatform's UIKit bindings reference iOS 17 symbols.
ARCHS must name the same slice -Pios.target linked. Gradle produces a single-architecture
NahalUI.framework, while xcodebuild -target — given no -destination to derive an active
architecture from — otherwise builds every applicable one and fails at Ld on the slice that has
no framework. Swap both together, or neither. (-derivedDataPath is not an option here either:
xcodebuild rejects it without -scheme, and this project has no committed scheme. Use SYMROOT to
redirect the output, as the release workflow does.)
The released asset is that same unsigned simulator build, Apple silicon only — install it with
xcrun simctl install booted NaHAL.app. A device build needs an Apple Developer signing identity,
which this repository deliberately does not handle.
To embed the navigator in your own app instead, take the Compose entry point from the nahal-ui
klib:
== Plugins
NaHAL ships without plugins, and activates nothing unless a configuration source names it — a plugin sitting on the classpath or in a drop-in directory stays inert on its own.
|=== | Runtime | How plugins get in | Ordering and properties
| JVM (desktop, Android)
| Drop *.jar into $NAHAL_PLUGINS_DIR (default plugins/ in the working directory). The app puts them on a child classloader; that is all it does.
| HALDISH_CONFIG — a JSON or YAML file listing plugin class names, read from a system property first and the environment second. File order is chain order; each entry's children are passed to the plugin's initialize().
| Native (macOS, iOS)
| No reflection: plugins are compiled in and registered by class name in CorePluginRegistry before the UI starts.
| HALDISH_CONFIG (JSON only) selects which registered plugins run, in what order.
| Browser (JS, Wasm)
| Registered in Kotlin via CorePluginRegistry by the hosting page's app code — no dynamic loading in a browser.
| window.__nahalConfig, the same nested tree as the config file.
| Any, single artifact
| HALDISH_PLUGIN_PATH with no HALDISH_CONFIG — Haldish's own loader takes that one artifact (a JAR on the JVM, a .dylib/.so/.dll on native). Naming the artifact is the configuration.
| Chain several by pointing at a chain artifact. Note the native C ABI carries no properties.
|===
Running the UI with plugins active is driven from the plugin repository, which owns those tasks — see https://github.com/C06A/HALDiSh_Plugins[C06A/HALDiSh_Plugins].
The plugin contract itself — hooks, lifecycle, the C ABI, packaging — is specified in link:PLUGIN_CONTRACT.md[PLUGIN_CONTRACT.md].
== Building and testing
The non-JVM targets of :core are verified by their own tasks, since they have no standard test
runner:
Editing core/src recompiles :ui and :testkit: :core is not published, and those modules
add its source directories to their own source sets rather than depending on a nahal-core
artifact. There is no artifact boundary to shield them.
There is no bootstrap step: Haldish comes from Maven Central like any other dependency, and nothing here consumes a plugin. Clone and build.
To try a Haldish change before it is released, publish it locally and add mavenLocal() to
settings.gradle.kts for as long as you need it:
== Release artifacts
stageReleaseArtifacts builds everything Gradle can produce on the current host (the Android leg
needs an SDK on ANDROID_HOME); the iOS asset is the exception — it comes from xcodebuild in the
release workflow, not from a Gradle task.
|=== | Asset | Contents
| NahalNavigator-<version>.dmg / .deb / .msi
| Desktop installer — host OS format only
| nahal-ui-web-<version>.zip
| Browser app bundle (JS) — runs anywhere
| nahal-ui-wasm-<version>.zip
| Browser app bundle (WebAssembly) — needs a WasmGC browser
| nahal-app-macos-{arm64,x64}-<version>.zip
| NaHAL.app — the JVM-free Kotlin/Native desktop app
| nahal-android-<version>.apk / .aab
| Android app; the APK is the installable download, the AAB is what a Play Store upload needs
| nahal-app-ios-simulator-<version>.zip
| Unsigned iOS simulator build of NaHAL.app — Apple silicon (arm64) only
| nahal-native-<platform>-<version>.zip
| nahal-core shared library + C header, for macos-arm64, macos-x64, linux-x64, windows-x64
| <asset>.sha256
| Checksum for every asset above
|===
No single machine builds the full set: Compose Desktop packages only the host's installer format,
each Kotlin/Native library links on its own host, and the iOS app needs Xcode.
.github/workflows/release.yml runs the per-host subsets on a tag matching the project version
(v2.1.0 ↔ version = "2.1.0" in the root build.gradle.kts), then collects everything into one
draft GitHub release.
Klibs, sources and javadoc are not duplicated as release assets — Maven Central serves those for
Gradle and Maven users, published separately with ./gradlew publish.
== Related tools
== License
link:https://www.apache.org/licenses/LICENSE-2.0[Apache License 2.0]
= NaHAL — HAL navigator :toc: left :toclevels: 3 :icons: font :source-highlighter: highlight.js
image:https://img.shields.io/badge/demo-live-2b6cb0[Live demo,link=https://c06a.github.io/NaHAL/] image:https://img.shields.io/maven-central/v/com.helpchoice.nahal/nahal-ui[Maven Central,link=https://central.sonatype.com/artifact/com.helpchoice.nahal/nahal-ui] image:https://img.shields.io/github/v/release/C06A/NaHAL[Latest release,link=https://github.com/C06A/NaHAL/releases/latest] image:https://img.shields.io/badge/license-Apache%202.0-blue[License,link=https://www.apache.org/licenses/LICENSE-2.0]
NaHAL is a graphical navigator for https://stateless.group/hal_specification.html[HAL (Hypertext Application Language)] APIs, plus the navigation layer it is built on, for https://kotlinlang.org/docs/multiplatform.html[Kotlin Multiplatform]. Point it at a HAL endpoint and walk the API by following links, expanding URI templates, inspecting embedded resources and building requests — on the desktop, on a phone, in a browser, or embedded in your own application.
The GUI offers two switchable layouts: panes (traversal rail, accordion resource viewer, request builder) and graph (a node-link canvas of the traversal with a detail drawer).
The HAL client library and the example plugins are not part of this repository.
com.helpchoice.nahal:haldish. Change the HAL client there, not here.:core, :ui, :androidApp, :testkit,
:testkit-groovy, plus the iosApp/ Xcode host.== What is in this repository
|=== | Module | Artifact | Role
| :core
| not published — its sources compile into nahal-ui and haldish-testkit
| Navigation layer over Haldish — HalNavigator, LinkSelector, DocLinkResolver, config-driven plugin loading, and JS / Wasm / C facades. No application entry point. Still builds the nahal-core shared library and C header shipped with the GitHub release.
| :ui
| com.helpchoice.nahal:nahal-ui
| The Compose Multiplatform navigator GUI: desktop (JVM and native macOS), browser (JS and Wasm), Android library variant, iOS NahalUI.framework. Library only — every platform's entry point calls NaHalNavigator().
| :androidApp
| not published — released as an APK/AAB
| The installable Android app: one Activity hosting NaHalNavigator().
| :testkit
| com.helpchoice.nahal:haldish-testkit
| Kotlin test DSL for driving HAL APIs in tests.
| :testkit-groovy
| com.helpchoice.nahal:haldish-testkit-groovy
| Groovy/Spock bindings for the testkit.
| iosApp/
| not a Gradle module
| Xcode project that links NahalUI.framework (produced by :ui) into an iOS app.
|===
== Supported platforms
|=== | Platform | What you get | How it is delivered
| macOS (JVM)
| Desktop app
| .dmg installer, or ./gradlew :ui:jvmRun
| macOS (native)
| Desktop app, no JVM required
| nahal-app-macos-{arm64,x64}-<version>.zip, or ./gradlew :ui:bundleMacosArm64App / :ui:bundleMacosX64App
| Linux
| Desktop app
| .deb installer, or ./gradlew :ui:jvmRun
| Windows
| Desktop app
| .msi installer, or ./gradlew :ui:jvmRun
| Browser (JS)
| Web app
| nahal-ui-web-<version>.zip, served from any static host
| Browser (Wasm)
| Web app, WasmGC browsers only
| nahal-ui-wasm-<version>.zip; see <<wasm-status,WebAssembly build>>
| Android
| Installable app (API 24+)
| nahal-android-<version>.apk, or ./gradlew :androidApp:assembleRelease
| iOS
| App, and an embeddable view controller
| nahal-app-ios-simulator-<version>.zip (unsigned Apple-silicon simulator build), or Xcode over NahalUI.framework; MainViewController() from the nahal-ui klib to embed it
| Linux / macOS / Windows (C)
| Shared library
| libnahal_core.{so,dylib,dll} + generated header
|===
== Running the app
=== Desktop, any OS (JVM)
Installers are built by Compose Desktop, which only produces the host format — a full set needs a CI matrix across the three operating systems:
./gradlew :ui:packageDistributionForCurrentOS
These installers embed a Java runtime, so the end user installs no JVM. Linux and Windows get one of them rather than a JVM-free native build, because Compose Multiplatform ships no Kotlin/Native renderer for either — macOS is the only desktop OS where the native path below exists.
=== macOS, without a JVM (Kotlin/Native)
The macOS targets also build a standalone executable and wrap it in an app bundle:
./gradlew :ui:runMacosArm64App # Apple silicon — builds NaHAL.app and opens it ./gradlew :ui:runMacosX64App # Intel
runMacos*App launches through open, and macOS LaunchServices does not pass the shell
environment to the app. Environment variables such as HALDISH_CONFIG set in a terminal will not
reach it — run the executable directly (ui/build/bin/macosArm64/releaseExecutable/ui.kexe) when
you need them.=== Browser
Nothing to install: the latest release is hosted at
https://c06a.github.io/NaHAL/[c06a.github.io/NaHAL] — a landing page linking the JS build (js/)
and the WebAssembly one (wasm/). Being served over HTTPS, that copy can only reach APIs that are
HTTPS and CORS-enabled for the origin, so for a HAL server on your own machine or network take
the bundle below and serve it from http://localhost.
Build the bundle, or take the released zip:
./gradlew :ui:jsBrowserDistribution
unzip nahal-ui-web-.zip -d nahal-web cd nahal-web python3 -m http.server 8080
The bundle contains index.html, the compiled ui.js, the Skia runtime (skiko.js, skiko.wasm) and composeResources/. Three things to know:
file:// fails, because skiko.js fetches skiko.wasm.application/wasm for .wasm (Python's http.server and modern nginx already do; older nginx needs types { application/wasm wasm; }).index.html are relative, so hosting under a sub-path works unchanged.Because a browser build is subject to the same-origin policy, every API you navigate to must return
Access-Control-Allow-Origin for your host. APIs that work in the desktop build can still fail in
the browser for this reason alone.
[[wasm-status]] ==== WebAssembly build
The same UI also compiles to WebAssembly, shipped as its own asset next to the JS one:
./gradlew :ui:wasmJsBrowserDistribution
It serves under the same rules as the JS bundle — HTTP only, application/wasm for .wasm,
relative paths — with two differences:
skiko.js bootstrap. Compose's Wasm runtime loads Skia itself, so index.html simply
loads ui.js; the Skia and application code arrive as two .wasm blobs.For 2.1.0 the Wasm zip is the smaller download — 4.3 MB against the JS bundle's 5.6 MB — while also keeping the draw path out of a JS interpreter. Browser support, not size, is what keeps the JS bundle in the release.
=== Android
:androidApp is the installable app; :ui carries the reusable android library variant that ships
inside nahal-ui. Minimum API level 24, compiled against API 35.
Release signing comes from NAHAL_KEYSTORE, NAHAL_KEYSTORE_PASSWORD, NAHAL_KEY_ALIAS and
NAHAL_KEY_PASSWORD (Gradle properties or environment variables). Without them the release build
falls back to the local debug key — installable from a GitHub download, not acceptable for a Play
Store upload.
=== iOS
Gradle links NahalUI.framework; Xcode turns it into the app. Building needs the iOS 17 SDK
(Xcode 15+), because Compose Multiplatform's UIKit bindings reference iOS 17 symbols.
ARCHS must name the same slice -Pios.target linked. Gradle produces a single-architecture
NahalUI.framework, while xcodebuild -target — given no -destination to derive an active
architecture from — otherwise builds every applicable one and fails at Ld on the slice that has
no framework. Swap both together, or neither. (-derivedDataPath is not an option here either:
xcodebuild rejects it without -scheme, and this project has no committed scheme. Use SYMROOT to
redirect the output, as the release workflow does.)
The released asset is that same unsigned simulator build, Apple silicon only — install it with
xcrun simctl install booted NaHAL.app. A device build needs an Apple Developer signing identity,
which this repository deliberately does not handle.
To embed the navigator in your own app instead, take the Compose entry point from the nahal-ui
klib:
== Plugins
NaHAL ships without plugins, and activates nothing unless a configuration source names it — a plugin sitting on the classpath or in a drop-in directory stays inert on its own.
|=== | Runtime | How plugins get in | Ordering and properties
| JVM (desktop, Android)
| Drop *.jar into $NAHAL_PLUGINS_DIR (default plugins/ in the working directory). The app puts them on a child classloader; that is all it does.
| HALDISH_CONFIG — a JSON or YAML file listing plugin class names, read from a system property first and the environment second. File order is chain order; each entry's children are passed to the plugin's initialize().
| Native (macOS, iOS)
| No reflection: plugins are compiled in and registered by class name in CorePluginRegistry before the UI starts.
| HALDISH_CONFIG (JSON only) selects which registered plugins run, in what order.
| Browser (JS, Wasm)
| Registered in Kotlin via CorePluginRegistry by the hosting page's app code — no dynamic loading in a browser.
| window.__nahalConfig, the same nested tree as the config file.
| Any, single artifact
| HALDISH_PLUGIN_PATH with no HALDISH_CONFIG — Haldish's own loader takes that one artifact (a JAR on the JVM, a .dylib/.so/.dll on native). Naming the artifact is the configuration.
| Chain several by pointing at a chain artifact. Note the native C ABI carries no properties.
|===
Running the UI with plugins active is driven from the plugin repository, which owns those tasks — see https://github.com/C06A/HALDiSh_Plugins[C06A/HALDiSh_Plugins].
The plugin contract itself — hooks, lifecycle, the C ABI, packaging — is specified in link:PLUGIN_CONTRACT.md[PLUGIN_CONTRACT.md].
== Building and testing
The non-JVM targets of :core are verified by their own tasks, since they have no standard test
runner:
Editing core/src recompiles :ui and :testkit: :core is not published, and those modules
add its source directories to their own source sets rather than depending on a nahal-core
artifact. There is no artifact boundary to shield them.
There is no bootstrap step: Haldish comes from Maven Central like any other dependency, and nothing here consumes a plugin. Clone and build.
To try a Haldish change before it is released, publish it locally and add mavenLocal() to
settings.gradle.kts for as long as you need it:
== Release artifacts
stageReleaseArtifacts builds everything Gradle can produce on the current host (the Android leg
needs an SDK on ANDROID_HOME); the iOS asset is the exception — it comes from xcodebuild in the
release workflow, not from a Gradle task.
|=== | Asset | Contents
| NahalNavigator-<version>.dmg / .deb / .msi
| Desktop installer — host OS format only
| nahal-ui-web-<version>.zip
| Browser app bundle (JS) — runs anywhere
| nahal-ui-wasm-<version>.zip
| Browser app bundle (WebAssembly) — needs a WasmGC browser
| nahal-app-macos-{arm64,x64}-<version>.zip
| NaHAL.app — the JVM-free Kotlin/Native desktop app
| nahal-android-<version>.apk / .aab
| Android app; the APK is the installable download, the AAB is what a Play Store upload needs
| nahal-app-ios-simulator-<version>.zip
| Unsigned iOS simulator build of NaHAL.app — Apple silicon (arm64) only
| nahal-native-<platform>-<version>.zip
| nahal-core shared library + C header, for macos-arm64, macos-x64, linux-x64, windows-x64
| <asset>.sha256
| Checksum for every asset above
|===
No single machine builds the full set: Compose Desktop packages only the host's installer format,
each Kotlin/Native library links on its own host, and the iOS app needs Xcode.
.github/workflows/release.yml runs the per-host subsets on a tag matching the project version
(v2.1.0 ↔ version = "2.1.0" in the root build.gradle.kts), then collects everything into one
draft GitHub release.
Klibs, sources and javadoc are not duplicated as release assets — Maven Central serves those for
Gradle and Maven users, published separately with ./gradlew publish.
== Related tools
== License
link:https://www.apache.org/licenses/LICENSE-2.0[Apache License 2.0]