
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
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, in a browser, or embedded in your own application.
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.com.helpchoice.nahal:haldish-plugin-<name>. They also own the tasks that launch NaHAL with plugins active.== What is in this repository
|=== | Module | Artifact | Role
| :core
| com.helpchoice.nahal:nahal-core
| Navigation layer over Haldish — HalNavigator, LinkSelector, DocLinkResolver, config-driven plugin loading, and JS / Wasm / C facades. No application entry point.
| :ui
| com.helpchoice.nahal:nahal-ui
| The Compose Multiplatform navigator GUI: desktop, browser, iOS.
| :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.
|===
== 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 bundle built by ./gradlew :ui:bundleMacosArm64App
| 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)
| — not shipped yet
| The wasmJs target builds klibs only; see <<wasm-status,Wasm status>>
| iOS
| Embeddable view controller
| MainViewController() from the nahal-ui klib; no packaged app in this build
| 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
=== 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
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]] ==== Wasm status
:ui declares wasmJs { browser() } with no binaries.executable(), so no webpack bundle or
distribution task exists for it — only the published klib. Add binaries.executable() to that
target to build a Wasm web app alongside the JS one.
=== iOS
:ui publishes iOS klibs and exposes a Compose entry point; this build produces no .app or
.ipa. Embed it from an Xcode project:
== 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
| 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. File order is chain order; each entry's children are passed to the plugin's initialize().
| Native
| No reflection: plugins are compiled in and registered by class name before the UI starts.
| HALDISH_CONFIG (JSON only) selects which registered plugins run, in what order.
| Browser
| Registered by the hosting page.
| window.__nahalConfig.
| 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.
|===
To run the UI with the example plugins chained (CURIE expansion → base-URL rewriting → request logging):
The plugin contract itself — hooks, lifecycle, the C ABI, packaging — is specified in link:PLUGIN_CONTRACT.md[PLUGIN_CONTRACT.md].
== Using nahal-core as a library
import com.helpchoice.nahal.core.HalNavigator import com.helpchoice.nahal.core.LinkSelector import com.helpchoice.nahal.core.RequestSpec
HalNavigator().use { navigator -> // Fetch a starting document val root = navigator.send(RequestSpec(url = "https://api.example.com/")).document!!
// Follow a link by rel — plugins' preLink hook runs, the URI template is expanded,
// the response is parsed
val orders = navigator.navigate(root, LinkSelector.TopLevel("orders"))
println("${orders.statusCode} ${orders.url}")
// Links inside embedded resources and collection items
navigator.navigate(root, LinkSelector.InEmbedded("ea:order", linkRel = "self"))
navigator.navigate(root, LinkSelector.InItems(itemIndex = 0, linkRel = "self"))
DocLinkResolver resolves a rel's documentation URL from the HAL curies relation, walking
outward through enclosing resources.
=== JavaScript / Node.js
The :core JS target publishes a library with an exported navigator facade:
On Wasm the same operations are module-level functions (coreLinkHref, coreEmbeddedLinkHref),
because Kotlin/Wasm restricts @JsExport to functions.
=== C / C++
./gradlew :core:linkReleaseSharedMacosArm64 # or …LinuxX64, …MingwX64, …MacosX64
The full HAL client C API — HTTP verbs, headers, multipart, URI templates — belongs to Haldish and is documented in https://github.com/C06A/HALDiSh_KMP[its repository].
== Building and testing
Because Maven Central currently serves only Haldish 1.0.1, building against the current 2.0.0 needs
it published locally first — settings.gradle.kts adds mavenLocal() for exactly this:
That is also the bootstrap order between the three repositories: Haldish → this build → plugins
(:testkit here consumes the CURIE plugin).
== Release artifacts
|=== | Asset | Contents
| NahalNavigator-<version>.dmg / .deb / .msi
| Desktop installer — host OS format only
| nahal-ui-web-<version>.zip
| Browser app bundle (JS)
| 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
|===
Attach all of them to the GitHub release. Klibs, sources and javadoc are not duplicated here — Maven Central serves those for Gradle and Maven users.
== 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
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, in a browser, or embedded in your own application.
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.com.helpchoice.nahal:haldish-plugin-<name>. They also own the tasks that launch NaHAL with plugins active.== What is in this repository
|=== | Module | Artifact | Role
| :core
| com.helpchoice.nahal:nahal-core
| Navigation layer over Haldish — HalNavigator, LinkSelector, DocLinkResolver, config-driven plugin loading, and JS / Wasm / C facades. No application entry point.
| :ui
| com.helpchoice.nahal:nahal-ui
| The Compose Multiplatform navigator GUI: desktop, browser, iOS.
| :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.
|===
== 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 bundle built by ./gradlew :ui:bundleMacosArm64App
| 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)
| — not shipped yet
| The wasmJs target builds klibs only; see <<wasm-status,Wasm status>>
| iOS
| Embeddable view controller
| MainViewController() from the nahal-ui klib; no packaged app in this build
| 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
=== 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
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]] ==== Wasm status
:ui declares wasmJs { browser() } with no binaries.executable(), so no webpack bundle or
distribution task exists for it — only the published klib. Add binaries.executable() to that
target to build a Wasm web app alongside the JS one.
=== iOS
:ui publishes iOS klibs and exposes a Compose entry point; this build produces no .app or
.ipa. Embed it from an Xcode project:
== 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
| 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. File order is chain order; each entry's children are passed to the plugin's initialize().
| Native
| No reflection: plugins are compiled in and registered by class name before the UI starts.
| HALDISH_CONFIG (JSON only) selects which registered plugins run, in what order.
| Browser
| Registered by the hosting page.
| window.__nahalConfig.
| 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.
|===
To run the UI with the example plugins chained (CURIE expansion → base-URL rewriting → request logging):
The plugin contract itself — hooks, lifecycle, the C ABI, packaging — is specified in link:PLUGIN_CONTRACT.md[PLUGIN_CONTRACT.md].
== Using nahal-core as a library
import com.helpchoice.nahal.core.HalNavigator import com.helpchoice.nahal.core.LinkSelector import com.helpchoice.nahal.core.RequestSpec
HalNavigator().use { navigator -> // Fetch a starting document val root = navigator.send(RequestSpec(url = "https://api.example.com/")).document!!
// Follow a link by rel — plugins' preLink hook runs, the URI template is expanded,
// the response is parsed
val orders = navigator.navigate(root, LinkSelector.TopLevel("orders"))
println("${orders.statusCode} ${orders.url}")
// Links inside embedded resources and collection items
navigator.navigate(root, LinkSelector.InEmbedded("ea:order", linkRel = "self"))
navigator.navigate(root, LinkSelector.InItems(itemIndex = 0, linkRel = "self"))
DocLinkResolver resolves a rel's documentation URL from the HAL curies relation, walking
outward through enclosing resources.
=== JavaScript / Node.js
The :core JS target publishes a library with an exported navigator facade:
On Wasm the same operations are module-level functions (coreLinkHref, coreEmbeddedLinkHref),
because Kotlin/Wasm restricts @JsExport to functions.
=== C / C++
./gradlew :core:linkReleaseSharedMacosArm64 # or …LinuxX64, …MingwX64, …MacosX64
The full HAL client C API — HTTP verbs, headers, multipart, URI templates — belongs to Haldish and is documented in https://github.com/C06A/HALDiSh_KMP[its repository].
== Building and testing
Because Maven Central currently serves only Haldish 1.0.1, building against the current 2.0.0 needs
it published locally first — settings.gradle.kts adds mavenLocal() for exactly this:
That is also the bootstrap order between the three repositories: Haldish → this build → plugins
(:testkit here consumes the CURIE plugin).
== Release artifacts
|=== | Asset | Contents
| NahalNavigator-<version>.dmg / .deb / .msi
| Desktop installer — host OS format only
| nahal-ui-web-<version>.zip
| Browser app bundle (JS)
| 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
|===
Attach all of them to the GitHub release. Klibs, sources and javadoc are not duplicated here — Maven Central serves those for Gradle and Maven users.
== Related tools
== License
link:https://www.apache.org/licenses/LICENSE-2.0[Apache License 2.0]