mhoennig f7ca150e1f
werkator build succeeded after 01:50
werkator/samples-linux-amd64 build succeeded after 02:49
Build downloadable AMD64 desktop samples after successful tests
2026-09-16 13:15:34 +02:00
2026-09-16 12:08:41 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 12:08:41 +02:00
2026-09-16 12:08:41 +02:00
2026-09-16 12:08:41 +02:00
2026-09-16 12:08:41 +02:00
2026-09-16 12:08:41 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 11:03:07 +02:00
2026-09-16 12:08:41 +02:00

Keluna

Kotlin Embedded Local UI for Native Applications

An initial Linux x86-64 Kotlin/Native framework prototype: Kotlin owns the UI tree, state and click handlers; the system WebView renders HTML and CSS. There is no HTTP server, bundled browser, Node dependency, or JVM in the resulting application. A JDK is required to run Gradle during development.

License

Keluna, including its original examples and documentation, is licensed under the Apache License 2.0. See also NOTICE.

Third-party code, dependencies, and tools retain their respective licenses; existing third-party notices take precedence for those files. See third-party code policy before copying or porting code.

Install a development JDK with SDKMAN!

Recommended: Eclipse Temurin JDK 25 (LTS). The project's Gradle 9.3.0 wrapper supports running on Java 25. Java 17 is only the minimum Gradle runtime version; use JDK 25 for a new development setup. Java is needed for the build tools, not for the compiled Keluna application.

The repository pins the JDK in .sdkmanrc. With SDKMAN! already installed, run:

source "${SDKMAN_DIR:-$HOME/.sdkman}/bin/sdkman-init.sh"
cd ~/Projekte/keluna
sdk env install
sdk env
java -version
./gradlew --version
./gradlew check

sdk env install installs the recorded JDK if necessary. sdk env selects it for the current shell. The global Java default does not need to change.

25.0.4-tem was available in SDKMAN!'s Linux x86-64 catalog on 2026-09-16. For later patch updates, use sdk list java to find a Temurin 25.x identifier and update the java= entry in .sdkmanrc. This is the single source of truth for both manual SDKMAN! activation and the included direnv configuration.

Automatic activation with direnv

The included .envrc loads SDKMAN!, watches .sdkmanrc for changes, and runs sdk env. It activates an installed JDK; it does not install missing versions. Install prerequisites once with sdk env install as shown above.

If direnv is not installed, on Ubuntu/Debian run sudo apt install direnv. Enable its shell hook once in your personal shell configuration:

# Bash: add to ~/.bashrc after SDKMAN! initialization
eval "$(direnv hook bash)"
# Zsh alternative: add to ~/.zshrc after SDKMAN! initialization
eval "$(direnv hook zsh)"

Use only the line matching your shell, then open a new terminal. Authorize the repository's .envrc locally:

cd ~/Projekte/keluna
direnv allow
java -version

Entering the repository now selects its JDK; leaving restores the previous environment. The shell hook and direnv allow are personal machine setup and cannot be enabled by checking in repository files. After an .envrc change, review it and run direnv allow again.

Without direnv, use sdk env manually, or enable sdkman_auto_env=true in your personal ~/.sdkman/etc/config. Choose one automatic activation mechanism; SDKMAN!'s auto-env is unnecessary when using direnv.

In IntelliJ IDEA, also select this JDK as the project's Gradle JVM. Gradle itself is supplied by ./gradlew; a separate SDKMAN! Gradle installation is unnecessary.

See the Gradle 9.3.0 Java compatibility matrix, Temurin support roadmap, and SDKMAN! usage documentation, and direnv shell setup.

Build and test the native core

Requires the development JDK above, an Internet connection on the first build, and a Linux x86-64 host. Gradle downloads Kotlin/Native and its toolchain.

./gradlew check
./gradlew :examples:counter:runDebugExecutableLinuxX64

Optional JavaScript runtime contract tests (Node.js 18+ for development only):

node --test keluna-web-runtime/runtime.test.cjs

The headless counter prints the initial DOM batch, then only the patches caused by a simulated button click. Tests also run as native executables.

Run the desktop counter

On Ubuntu 24.04 / Debian with suitable packages:

sudo apt install build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev
./gradlew -Pwebview=true :keluna-platform-linux:runDebugExecutableLinuxX64

Run from a graphical desktop session. Desktop configuration is opt-in so GTK headers are not required for framework development. Build a release executable:

./gradlew -Pwebview=true :keluna-platform-linux:linkReleaseExecutableLinuxX64

Output: keluna-platform-linux/build/bin/linuxX64/releaseExecutable/keluna-platform-linux.kexe. The HTML, framework JavaScript and CSS are embedded into the native executable. GTK 3 and WebKitGTK 4.1 remain system runtime dependencies on the target machine.

Play Snake

Snake is a native Keluna example with its game rules, state, scoring and timer callbacks in Kotlin. CSS controls presentation; the framework's browser runtime only updates DOM cells and forwards input. It uses the same Linux dependencies as the desktop counter.

./gradlew -Pwebview=true :keluna-platform-linux:runSnakeDebugExecutableLinuxX64

Controls: arrow keys / W A S D steer, Space / Enter start or pause, and R starts a new game. Buttons provide the same actions. Losing window focus pauses a running game automatically. Collect food for 10 points, avoid walls and your own body, and fill the 20 × 20 board to win. The best score lasts for the current application session. The game moves eight times per second.

Build an optimized executable once, then launch it directly without Gradle:

./gradlew -Pwebview=true :keluna-platform-linux:linkSnakeReleaseExecutableLinuxX64
./keluna-platform-linux/build/bin/linuxX64/snakeReleaseExecutable/snake.kexe

The debug executable can also be launched directly:

./keluna-platform-linux/build/bin/linuxX64/snakeDebugExecutable/snake.kexe

Game logic and UI live in examples/snake/src/commonMain/kotlin/dev/keluna/snake/: SnakeGame.kt, SnakeView.kt and adjacent Snake.css. The Linux entry point is keluna-platform-linux/src/linuxX64Main/kotlin/dev/keluna/snake/Main.kt. Snake and Counter share the window host and native adapter. GTK schedules ticks on the UI thread and removes the timer when the window closes. The DOM grid is created once; ticks send only changed cell classes and labels.

The eight game/view tests cover movement, pause, growth, walls, self-collision, turn buffering, moving into the vacating tail, winning and incremental patches. They require no graphical session or GTK development packages:

./gradlew :examples:snake:linuxX64Test

Workshop registration form

This example uses the new Vaadin-oriented component API: Component, typed fields, HasValue<T>, HasValidation, value-change events and a buffered Binder. See the API guide for semantics, examples and current limits. This direction is also recorded in AGENTS.md for future development.

./gradlew -Pwebview=true :keluna-platform-linux:runWorkshopDebugExecutableLinuxX64

Or build once and run the optimized native application directly:

./gradlew -Pwebview=true :keluna-platform-linux:linkWorkshopReleaseExecutableLinuxX64
./keluna-platform-linux/build/bin/linuxX64/workshopReleaseExecutable/workshop.kexe

The form demonstrates text, email, telephone, date, time, integer, select, radio-button, slider, textarea and checkbox inputs. Required fields, ranges, conversion errors and custom rules are validated in Kotlin with field-level messages. Catering is visible/required only for on-site attendance. A valid submission displays a typed local summary and total price; editing invalidates that preview. No booking is sent or stored. Reset restores defaults, and “Beispiel ausfüllen” loads sample data while leaving consent unchecked.

Application code is in examples/workshop/src/commonMain/kotlin/dev/keluna/workshop/: WorkshopForm.kt, WorkshopRegistration.kt, and adjacent WorkshopForm.css. It uses no HTML tags, raw attributes or JavaScript. Components can be composed before UiTree.mount(view). Input synchronization preserves DOM identity, caret position and IME composition, and prevents stale updates from replacing newer edits.

Run native component/binding/form tests without GTK development packages:

./gradlew :keluna-ui:linuxX64Test :examples:workshop:linuxX64Test
node keluna-web-runtime/runtime.test.cjs

Project layout

  • keluna-core: UI-thread-confined elements, stable internal IDs, change queue and JSON patches.
  • keluna-ui: typed components, field values, buffered Binder and Kotlin DSL helpers.
  • keluna-runtime: click dispatch and incremental synchronization.
  • keluna-web-runtime: internal DOM interpreter and framework CSS defaults.
  • keluna-platform-linux: small C interop adapter for GTK/WebKitGTK and desktop entry point.
  • examples/shared: shared Kotlin counter and adjacent component CSS, plus the application theme.
  • examples/counter: headless native counter executable.
  • examples/snake: pure Kotlin Snake game model, Keluna UI, colocated CSS and native tests.
  • examples/workshop: Vaadin-oriented registration form, data model, CSS and native tests.
  • docs: original concept documents; proposals, not claims of completed functionality.

Styling

CSS loads in this order: framework defaults, application theme, component CSS. Use semantic classes, explicit DOM IDs for accessibility, and CSS custom properties for theming. data-keluna-id is reserved for internal synchronization. Counter.css uses explicit data-keluna-scope selectors. Automatic CSS selector rewriting and a reusable resource/Gradle plugin are future work; the prototype's resource task embeds each demo's known stylesheets without a JavaScript bundler.

Validation

With Temurin JDK 25.0.4 on Ubuntu 24.04, all 34 Kotlin/Native tests and all nine JavaScript runtime tests passed. The Counter and Snake debug executables and Workshop debug/release executables built successfully. ldd -r found no missing libraries or unresolved symbols in the Workshop release executable.

Native-window checks in an isolated X11 display verified Workshop field errors, editing within text without moving the caret, valid submission previews, online attendance with hidden catering, and reset. The release executable also passed the sample-data/consent/validation workflow. The demo keeps all data local.

Regression checks verified Snake's movement, keyboard turns, pause/resume, wall collision, restart and automatic pause when focus moves to another window. Clicking Increment in Counter still changes the value from 0 to 1.

Linux linking and Java warnings

Kotlin/Native invokes the linker directly. The Linux build retains system library paths from pkg-config --keep-system-libs --libs, translates -pthread into -lpthread, and unwraps compiler-driver -Wl, options before passing them on. C compilation continues to use the original pkg-config --cflags output.

The host's GTK/WebKitGTK shared libraries may require newer glibc symbols than Kotlin/Native's bundled sysroot provides. --allow-shlib-undefined lets those shared-library imports resolve against the host at runtime; unresolved symbols in application object files remain link errors. This does not provide compatibility with older Linux distributions: the target system must satisfy the installed GTK/WebKitGTK libraries' ABI requirements. Verify dependencies on each target:

ldd -r keluna-platform-linux/build/bin/linuxX64/debugExecutable/keluna-platform-linux.kexe

Kotlin/Native 2.3.21's interop tools can emit sun.misc.Unsafe and restricted System.load warnings under JDK 25. These warnings did not prevent the verified build and are separate from linker errors. Disabling compiler caches is not required for this linking fix.

Prototype boundaries and next steps

Currently implemented: one Linux window, local content, initial mount, text, attributes, DOM properties, click/key/focus/input/change events, typed components, field validation, buffered form binding, and a UI-thread timer with incremental updates. State and callbacks are confined to the GTK main thread. The inbound event wire format is click:<node id>, key:<registered key>, blur and revisioned field values; see the API guide for the value envelope. Outbound updates are JSON arrays. Text uses textContent. Navigation away from the local document is blocked.

Not yet implemented: removal/reparenting and lifecycle disposal, dynamic data providers, reactive state/coroutines, multiple windows, native dialogs, packaging, macOS WKWebView and Windows WebView2 adapters. No cross-platform support or size, startup-time or memory claims have been measured yet. Next steps are lifecycle and richer components, packaging, and additional platform adapters.

Toolchain: Kotlin 2.3.21, Gradle 9.3.0. Kotlin's 2.3.20 line documents Gradle 9.3 compatibility: https://kotlinlang.org/docs/whatsnew2320.html#compatibility-with-gradle-9-3-0 WebKitGTK bridge API: https://webkitgtk.org/reference/webkit2gtk/stable/signal.UserContentManager.script-message-received.html

CI desktop downloads

Werkator builds the AMD64 desktop examples as the samples-linux-amd64 follow-up after the tests pass. Download the sample archive from the Keluna build history. See the sample guide for runtime requirements, usage, and the build environment.

S
Description
Keluna – Kotlin Embedded Local UI for Native Applications. A Kotlin/Native UI framework with a Vaadin Flow-inspired component API, rendered in the local system WebView. No HTTP server, JVM or Node at runtime. Apache-2.0.
Readme Apache-2.0
171 KiB
Languages
Kotlin 71.5%
JavaScript 16%
C 5.7%
Shell 4.1%
CSS 2.7%