changeset 642:05b585720d3b

core: reimplement most of js
author David Demelier <markand@malikania.fr>
date Sun, 17 Dec 2023 09:50:36 +0100
parents fcd124e513ea
children 993d9ccedcf6
files CMakeLists.txt libmlk-core/CMakeLists.txt libmlk-core/libmlk-core-js-config.cmake libmlk-core/mlk/core/js/js-animation.c libmlk-core/mlk/core/js/js-animation.h libmlk-core/mlk/core/js/js-clock.c libmlk-core/mlk/core/js/js-clock.h libmlk-core/mlk/core/js/js-event.c libmlk-core/mlk/core/js/js-event.h libmlk-core/mlk/core/js/js-font.c libmlk-core/mlk/core/js/js-font.h libmlk-core/mlk/core/js/js-music.c libmlk-core/mlk/core/js/js-music.h libmlk-core/mlk/core/js/js-painter.c libmlk-core/mlk/core/js/js-painter.h libmlk-core/mlk/core/js/js-sound.c libmlk-core/mlk/core/js/js-sound.h libmlk-core/mlk/core/js/js-sprite.c libmlk-core/mlk/core/js/js-sprite.h libmlk-core/mlk/core/js/js-texture.c libmlk-core/mlk/core/js/js-texture.h libmlk-core/mlk/core/js/js-util.c libmlk-core/mlk/core/js/js-util.h libmlk-core/mlk/core/js/js-window.c libmlk-core/mlk/core/js/js-window.h libmlk-core/mlk/core/js/js.c libmlk-core/mlk/core/js/js.h libmlk-core/mlk/core/js/main.c mlk-run/CMakeLists.txt mlk-run/mlk-run.c
diffstat 30 files changed, 2505 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Sun Oct 01 09:18:01 2023 +0200
+++ b/CMakeLists.txt	Sun Dec 17 09:50:36 2023 +0100
@@ -120,6 +120,10 @@
 	add_subdirectory(examples)
 endif ()
 
+if (MLK_WITH_JAVASCRIPT)
+	add_subdirectory(mlk-run)
+endif ()
+
 configure_file(
 	${molko_SOURCE_DIR}/cmake/MlkOptions.install.cmake
 	${molko_BINARY_DIR}/cmake/MlkOptions.cmake
--- a/libmlk-core/CMakeLists.txt	Sun Oct 01 09:18:01 2023 +0200
+++ b/libmlk-core/CMakeLists.txt	Sun Dec 17 09:50:36 2023 +0100
@@ -153,6 +153,28 @@
 if (MLK_WITH_JAVASCRIPT)
 	set(
 		JS_SOURCES
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-animation.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-animation.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-clock.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-clock.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-event.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-event.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-font.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-font.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-music.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-music.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-painter.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-painter.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sound.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sound.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sprite.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-sprite.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-texture.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-texture.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-util.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-util.h
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-window.c
+		${libmlk-core_SOURCE_DIR}/mlk/core/js/js-window.h
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js.c
 		${libmlk-core_SOURCE_DIR}/mlk/core/js/js.h
 	)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/libmlk-core-js-config.cmake	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,23 @@
+#
+# libmlk-core-js-config.cmake -- export file for libmlk-core-js
+#
+# Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+include(CMakeFindDependencyMacro)
+
+find_dependency(libmlk-core)
+
+include("${CMAKE_CURRENT_LIST_DIR}/libmlk-core-js-targets.cmake")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-animation.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,158 @@
+/*
+ * js-animation.c -- basic animations (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/err.h>
+#include <mlk/core/animation.h>
+
+#include "js-animation.h"
+#include "js-sprite.h"
+#include "js.h"
+
+#define SYMBOL          DUK_HIDDEN_SYMBOL("mlk::animation")
+#define SPRITE_REF      DUK_HIDDEN_SYMBOL("mlk::animation::sprite")
+
+static struct mlk_animation *
+mlk_js_animation_this(duk_context *ctx)
+{
+	struct mlk_animation *an;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	an = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!an)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not an Animation object");
+
+	return an;
+}
+
+static duk_ret_t
+mlk_js_animation_completed(duk_context *ctx)
+{
+	duk_push_boolean(ctx, mlk_animation_completed(mlk_js_animation_this(ctx)));
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_animation_new(duk_context *ctx)
+{
+	struct mlk_animation *anim;
+	struct mlk_sprite *sprite;
+	unsigned int delay;
+
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Animation must be new-constructed");
+
+	sprite = mlk_js_sprite_require(ctx, 0);
+	delay = duk_require_uint(ctx, 1);
+
+	anim = mlk_alloc_new0(1, sizeof (*anim));
+	anim->sprite = sprite;
+	anim->delay = delay;
+	mlk_animation_start(anim);
+
+	duk_push_this(ctx);
+	duk_push_pointer(ctx, anim);
+	duk_put_prop_string(ctx, -2, SYMBOL);
+	duk_push_string(ctx, "completed");
+	duk_push_c_function(ctx, mlk_js_animation_completed, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+
+	/* We store the texture into the sprite to avoid being collected. */
+	duk_dup(ctx, 0);
+	duk_put_prop_string(ctx, -2, SPRITE_REF);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_animation_finish(duk_context *ctx)
+{
+	duk_get_prop_string(ctx, 0, SYMBOL);
+	mlk_alloc_free(duk_to_pointer(ctx, -1));
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+	duk_del_prop_string(ctx, 0, SPRITE_REF);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_animation_update(duk_context *ctx)
+{
+	struct mlk_animation *anim = mlk_js_animation_this(ctx);
+	unsigned int ticks = duk_require_uint(ctx, 0);
+
+	duk_push_boolean(ctx, mlk_animation_update(anim, ticks));
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_animation_draw(duk_context *ctx)
+{
+	struct mlk_animation *anim = mlk_js_animation_this(ctx);
+	int x, y;
+
+	if (duk_get_top(ctx) == 2) {
+		x = duk_require_int(ctx, 0);
+		y = duk_require_int(ctx, 1);
+	} else if (duk_get_top(ctx) == 1) {
+		duk_require_object(ctx, 0);
+		duk_get_prop_string(ctx, 0, "x");
+		x = duk_get_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y");
+		y = duk_get_int(ctx, -1);
+		duk_pop_n(ctx, 2);
+	} else
+		return duk_error(ctx, DUK_ERR_ERROR, "Object or 2 numbers expected");
+
+	if (!mlk_animation_draw(anim, x, y))
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_function_list_entry methods[] = {
+	{ "update",     mlk_js_animation_update,        1               },
+	{ "draw",       mlk_js_animation_draw,          DUK_VARARGS     },
+	{ NULL,         NULL,                           0               }
+};
+
+void
+mlk_js_animation_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_animation_new, 3);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_animation_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Animation");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-animation.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-animation.h -- basic animations (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_ANIMATION_H
+#define MLK_CORE_JS_ANIMATION_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_animation_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_ANIMATION_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-clock.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,110 @@
+/*
+ * js-clock.c -- track elapsed time (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/clock.h>
+
+#include "js-window.h"
+#include "js.h"
+
+#define SYMBOL DUK_HIDDEN_SYMBOL("mlk::clock")
+
+static struct mlk_clock *
+mlk_js_clock_this(duk_context *ctx)
+{
+	struct mlk_clock *clock;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	clock = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!clock)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a Clock object");
+
+	return clock;
+}
+
+static duk_ret_t
+mlk_js_clock_getElapsed(duk_context *ctx)
+{
+	duk_push_uint(ctx, mlk_clock_elapsed(mlk_js_clock_this(ctx)));
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_clock_new(duk_context *ctx)
+{
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Clock must be new-constructed");
+
+	duk_push_this(ctx);
+	duk_push_pointer(ctx, mlk_alloc_new0(1, sizeof (struct mlk_clock)));
+	duk_put_prop_string(ctx, -2, SYMBOL);
+	duk_push_string(ctx, "elapsed");
+	duk_push_c_function(ctx, mlk_js_clock_getElapsed, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_clock_start(duk_context *ctx)
+{
+	mlk_clock_start(mlk_js_clock_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_clock_finish(duk_context *ctx)
+{
+	duk_get_prop_string(ctx, 0, SYMBOL);
+	mlk_alloc_free(duk_to_pointer(ctx, -1));
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+
+	return 0;
+}
+
+static const duk_function_list_entry methods[] = {
+	{ "start",      mlk_js_clock_start,     0 },
+	{ NULL,         NULL,                   0 }
+};
+
+void
+mlk_js_clock_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_clock_new, 0);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_clock_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Clock");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-clock.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-clock.h -- track elapsed time (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_CLOCK_H
+#define MLK_CORE_JS_CLOCK_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_clock_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_CLOCK_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-event.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,103 @@
+/*
+ * js-event.c -- event management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <duktape.h>
+
+#include <mlk/core/event.h>
+
+#include "js-event.h"
+#include "js.h"
+
+static duk_ret_t
+push(duk_context *ctx, const union mlk_event *ev)
+{
+	duk_push_object(ctx);
+	duk_push_int(ctx, ev->type);
+	duk_put_prop_string(ctx, -2, "type");
+
+	switch (ev->type) {
+	case MLK_EVENT_CLICKDOWN:
+	case MLK_EVENT_CLICKUP:
+		duk_push_int(ctx, ev->click.button);
+		duk_put_prop_string(ctx, -2, "button");
+		duk_push_int(ctx, ev->click.x);
+		duk_put_prop_string(ctx, -2, "x");
+		duk_push_int(ctx, ev->click.y);
+		duk_put_prop_string(ctx, -2, "y");
+		break;
+	case MLK_EVENT_KEYDOWN:
+	case MLK_EVENT_KEYUP:
+		duk_push_int(ctx, ev->key.key);
+		duk_put_prop_string(ctx, -2, "key");
+		break;
+	case MLK_EVENT_MOUSE:
+		duk_push_int(ctx, ev->mouse.buttons);
+		duk_put_prop_string(ctx, -2, "buttons");
+		duk_push_int(ctx, ev->mouse.x);
+		duk_put_prop_string(ctx, -2, "x");
+		duk_push_int(ctx, ev->mouse.y);
+		duk_put_prop_string(ctx, -2, "y");
+		break;
+	default:
+		break;
+	}
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_event_poll(duk_context *ctx)
+{
+	union mlk_event ev;
+
+	if (!mlk_event_poll(&ev))
+		return 0;
+
+	return push(ctx, &ev);
+}
+
+static const duk_number_list_entry types[] = {
+	{ "CLICK_DOWN", MLK_EVENT_CLICKDOWN },
+	{ "CLICK_UP",   MLK_EVENT_CLICKUP   },
+	{ "KEY_DOWN",   MLK_EVENT_KEYDOWN   },
+	{ "KEY_UP",     MLK_EVENT_KEYUP     },
+	{ "MOUSE",      MLK_EVENT_MOUSE     },
+	{ "QUIT",       MLK_EVENT_QUIT      },
+	{ NULL,         0               }
+};
+
+static const duk_function_list_entry functions[] = {
+	{ "poll",       mlk_js_event_poll,      0 },
+	{ NULL,         NULL,                   0 }
+};
+
+void
+mlk_js_event_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_object(ctx);
+	duk_put_number_list(ctx, -1, types);
+	duk_put_function_list(ctx, -1, functions);
+	duk_put_prop_string(ctx, -2, "Event");
+	duk_pop_n(ctx, 1);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-event.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-event.h -- event management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_EVENT_H
+#define MLK_CORE_JS_EVENT_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_event_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_EVENT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-font.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,164 @@
+/*
+ * js-font.c -- basic font management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <duktape.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/err.h>
+#include <mlk/core/font.h>
+#include <mlk/core/texture.h>
+
+#include "js-font.h"
+#include "js-texture.h"
+#include "js.h"
+
+#define SYMBOL  DUK_HIDDEN_SYMBOL("mlk::font")
+#define PROTO   DUK_HIDDEN_SYMBOL("mlk::font::prototype")
+
+static struct mlk_font *
+mlk_js_font_this(duk_context *ctx)
+{
+	struct mlk_font *font;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	font = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!font)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a Font object");
+
+	return font;
+}
+
+static duk_ret_t
+mlk_js_font_getHeight(duk_context *ctx)
+{
+	duk_push_uint(ctx, mlk_font_height(mlk_js_font_this(ctx)));
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_font_fromPath(duk_context *ctx)
+{
+	struct mlk_font font;
+	const char *path = duk_require_string(ctx, 0);
+	unsigned int size = duk_require_uint(ctx, 1);
+
+	if (!mlk_font_open(&font, path, size))
+		duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	duk_push_object(ctx);
+	duk_push_global_stash(ctx);
+	duk_get_prop_string(ctx, -1, PROTO);
+	duk_remove(ctx, -2);
+	duk_push_pointer(ctx, mlk_alloc_dup(&font, 1, sizeof (font)));
+	duk_put_prop_string(ctx, -3, SYMBOL);
+	duk_set_prototype(ctx, -2);
+	duk_push_string(ctx, "height");
+	duk_push_c_function(ctx, mlk_js_font_getHeight, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+	
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_font_render(duk_context *ctx)
+{
+	struct mlk_font *font = mlk_js_font_this(ctx);
+	struct mlk_texture tex;
+	const char *text = duk_require_string(ctx, 0);
+	unsigned long color = duk_require_number(ctx, 1);
+
+	if (mlk_font_render(font, &tex, text, color) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	mlk_js_texture_push(ctx, &tex);
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_font_query(duk_context *ctx)
+{
+	struct mlk_font *font = mlk_js_font_this(ctx);
+	const char *text = duk_require_string(ctx, 0);
+	unsigned int w, h;
+
+	if (mlk_font_query(font, text, &w, &h) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	duk_push_object(ctx);
+	duk_push_uint(ctx, w);
+	duk_put_prop_string(ctx, -2, "w");
+	duk_push_uint(ctx, h);
+	duk_put_prop_string(ctx, -2, "h");
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_font_finish(duk_context *ctx)
+{
+	struct mlk_font *font;
+
+	duk_get_prop_string(ctx, 0, SYMBOL);
+	font = duk_to_pointer(ctx, -1);
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+
+	if (font) {
+		mlk_font_finish(font);
+		mlk_alloc_free(font);
+	}
+
+	return 0;
+}
+
+static const duk_function_list_entry functions[] = {
+	{ "fromPath",   mlk_js_font_fromPath,   2 },
+	{ NULL,         NULL,                   0 }
+};
+
+static const duk_function_list_entry methods[] = {
+	{ "render",     mlk_js_font_render,     2 },
+	{ "query",      mlk_js_font_query,      1 },
+	{ NULL, NULL, 0 }
+};
+
+void
+mlk_js_font_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, functions);
+	duk_put_prop_string(ctx, -2, "Font");
+	duk_push_global_stash(ctx);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_font_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_put_prop_string(ctx, -2, PROTO);
+	duk_pop_n(ctx, 3);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-font.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-font.h -- basic font management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_FONT_H
+#define MLK_CORE_JS_FONT_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_font_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_FONT_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-music.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,156 @@
+/*
+ * js-music.c -- music support (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/err.h>
+#include <mlk/core/music.h>
+
+#include "js-music.h"
+
+#define SYMBOL DUK_HIDDEN_SYMBOL("mlk::music")
+
+static struct mlk_music *
+mlk_js_music_this(duk_context *ctx)
+{
+	struct mlk_music *mus;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	mus = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!mus)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a Music object");
+
+	return mus;
+}
+
+static duk_ret_t
+mlk_js_music_new(duk_context *ctx)
+{
+	struct mlk_music mus;
+	const char *path;
+
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Music must be new-constructed");
+
+	path = duk_require_string(ctx, 0);
+
+	if (mlk_music_open(&mus, path) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	duk_push_this(ctx);
+	duk_push_pointer(ctx, mlk_alloc_dup(&mus, 1, sizeof (mus)));
+	duk_put_prop_string(ctx, -2, SYMBOL);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_music_pause(duk_context *ctx)
+{
+	mlk_music_pause(mlk_js_music_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_music_resume(duk_context *ctx)
+{
+	mlk_music_resume(mlk_js_music_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_music_stop(duk_context *ctx)
+{
+	mlk_music_stop(mlk_js_music_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_music_play(duk_context *ctx)
+{
+	struct mlk_music *mus = mlk_js_music_this(ctx);
+	enum mlk_music_flags flags = duk_get_uint(ctx, 0);
+
+	if (mlk_music_play(mus, flags) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_music_finish(duk_context *ctx)
+{
+	struct mlk_music *mus;
+
+	duk_get_prop_string(ctx, 0, SYMBOL);
+
+	if ((mus = duk_to_pointer(ctx, -1))) {
+		mlk_music_finish(mus);
+		mlk_alloc_free(mus);
+	}
+
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+
+	return 0;
+}
+
+static const duk_number_list_entry flags[] = {
+	{ "NONE",       MLK_MUSIC_NONE                          },
+	{ "LOOP",       MLK_MUSIC_LOOP                          },
+	{ NULL,         0                                       }
+};
+
+static const duk_function_list_entry functions[] = {
+	{ "pause",      mlk_js_music_pause,     0               },
+	{ "resume",     mlk_js_music_resume,    0               },
+	{ "stop",       mlk_js_music_stop,      DUK_VARARGS     },
+	{ NULL,         NULL,                   0               }
+};
+
+static const duk_function_list_entry methods[] = {
+	{ "play",       mlk_js_music_play,      DUK_VARARGS     },
+	{ NULL,         NULL,                   0               }
+};
+
+void
+mlk_js_music_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_music_new, 3);
+	duk_put_function_list(ctx, -1, functions);
+	duk_put_number_list(ctx, -1, flags);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_music_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Music");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-music.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-music.h -- music support (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_MUSIC_H
+#define MLK_CORE_JS_MUSIC_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_music_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_JS_MUSIC_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-painter.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,211 @@
+/*
+ * js-painter.c -- basic drawing routines (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <duktape.h>
+
+#include <mlk/core/painter.h>
+
+#include "js.h"
+#include "js-painter.h"
+
+#define SYMBOL          DUK_HIDDEN_SYMBOL("mlk::sprite")
+#define TEXTURE_REF     DUK_HIDDEN_SYMBOL("mlk::sprite::texture")
+
+static duk_ret_t
+mlk_js_painter_getColor(duk_context *ctx)
+{
+	duk_push_number(ctx, mlk_painter_get_color());
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_painter_setColor(duk_context *ctx)
+{
+	mlk_painter_set_color(duk_require_number(ctx, 0));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_new(duk_context *ctx)
+{
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Painter must be new-constructed");
+
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_clear(duk_context *ctx)
+{
+	(void)ctx;
+
+	mlk_painter_clear();
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_drawLine(duk_context *ctx)
+{
+	int x1, y1, x2, y2;
+
+	if (duk_get_top(ctx) == 4) {
+		x1 = duk_require_int(ctx, 0);
+		y1 = duk_require_int(ctx, 1);
+		x2 = duk_require_int(ctx, 2);
+		y2 = duk_require_int(ctx, 3);
+	} else if (duk_get_top(ctx) == 1) {
+		duk_require_object(ctx, 0);
+		duk_get_prop_string(ctx, 0, "x1");
+		x1 = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y1");
+		y1 = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "x2");
+		x2 = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y2");
+		y2 = duk_require_int(ctx, -1);
+		duk_pop_n(ctx, 4);
+	} else
+		return duk_error(ctx, DUK_ERR_ERROR, "Object or 4 numbers expected");
+
+	mlk_painter_draw_line(x1, y2, x2, y2);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_drawPoint(duk_context *ctx)
+{
+	int x, y;
+
+	if (duk_get_top(ctx) == 2) {
+		x = duk_require_int(ctx, 0);
+		y = duk_require_int(ctx, 1);
+	} else if (duk_get_top(ctx) == 1) {
+		duk_require_object(ctx, 0);
+		duk_get_prop_string(ctx, 0, "x");
+		x = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y");
+		y = duk_require_int(ctx, -1);
+		duk_pop_n(ctx, 2);
+	} else
+		return duk_error(ctx, DUK_ERR_ERROR, "Object or 2 numbers expected");
+
+	mlk_painter_draw_point(x, y);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_drawRectangle(duk_context *ctx)
+{
+	int x, y;
+	unsigned int w, h;
+
+	if (duk_get_top(ctx) == 4) {
+		x = duk_require_int(ctx, 0);
+		y = duk_require_int(ctx, 1);
+		w = duk_require_uint(ctx, 2);
+		h = duk_require_uint(ctx, 3);
+	} else if (duk_get_top(ctx) == 1) {
+		duk_require_object(ctx, 0);
+		duk_get_prop_string(ctx, 0, "x");
+		x = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y");
+		y = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "w");
+		w = duk_require_uint(ctx, -1);
+		duk_get_prop_string(ctx, 0, "h");
+		h = duk_require_uint(ctx, -1);
+		duk_pop_n(ctx, 4);
+	} else
+		return duk_error(ctx, DUK_ERR_ERROR, "Object or 4 numbers expected");
+
+	mlk_painter_draw_rectangle(x, y, w, h);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_drawCircle(duk_context *ctx)
+{
+	int x, y;
+	double r;
+
+	if (duk_get_top(ctx) == 3) {
+		x = duk_require_int(ctx, 0);
+		y = duk_require_int(ctx, 1);
+		r = duk_require_number(ctx, 2);
+	} else if (duk_get_top(ctx) == 1) {
+		duk_require_object(ctx, 0);
+		duk_get_prop_string(ctx, 0, "x");
+		x = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y");
+		y = duk_require_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "r");
+		r = duk_require_number(ctx, -1);
+		duk_pop_n(ctx, 3);
+	} else
+		return duk_error(ctx, DUK_ERR_ERROR, "Object or 3 numbers expected");
+
+	mlk_painter_draw_circle(x, y, r);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_painter_present(duk_context *ctx)
+{
+	(void)ctx;
+
+	mlk_painter_present();
+
+	return 0;
+}
+
+static const duk_function_list_entry methods[] = {
+	{ "clear",              mlk_js_painter_clear,           0               },
+	{ "drawLine",           mlk_js_painter_drawLine,        DUK_VARARGS     },
+	{ "drawPoint",          mlk_js_painter_drawPoint,       DUK_VARARGS     },
+	{ "drawRectangle",      mlk_js_painter_drawRectangle,   DUK_VARARGS     },
+	{ "drawCircle",         mlk_js_painter_drawCircle,      DUK_VARARGS     },
+	{ "present",            mlk_js_painter_present,         0               },
+	{ NULL,                 NULL,                           0               }
+};
+
+void
+js_painter_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_object(ctx);
+	duk_push_string(ctx, "color");
+	duk_push_c_function(ctx, mlk_js_painter_getColor, 0);
+	duk_push_c_function(ctx, mlk_js_painter_setColor, 1);
+	duk_def_prop(ctx, -4, DUK_DEFPROP_HAVE_GETTER | DUK_DEFPROP_HAVE_SETTER);
+	duk_put_function_list(ctx, -1, methods);
+	duk_put_prop_string(ctx, -2, "Painter");
+	duk_pop(ctx);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-painter.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-painter.h -- basic drawing routines (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_PAINTER_H
+#define MLK_CORE_JS_PAINTER_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_painter_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_PAINTER_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-sound.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,143 @@
+/*
+ * js-sound.c -- sound support (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/err.h>
+#include <mlk/core/sound.h>
+
+#include "js-sound.h"
+
+#define SYMBOL DUK_HIDDEN_SYMBOL("mlk::sound")
+
+static struct mlk_sound *
+mlk_js_sound_this(duk_context *ctx)
+{
+	struct mlk_sound *snd;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	snd = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!snd)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a Sound object");
+
+	return snd;
+}
+
+static duk_ret_t
+mlk_js_sound_new(duk_context *ctx)
+{
+	struct mlk_sound snd;
+	const char *path;
+
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Sound must be new-constructed");
+
+	path = duk_require_string(ctx, 0);
+
+	if (mlk_sound_open(&snd, path) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	duk_push_this(ctx);
+	duk_push_pointer(ctx, mlk_alloc_dup(&snd, 1, sizeof (snd)));
+	duk_put_prop_string(ctx, -2, SYMBOL);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sound_play(duk_context *ctx)
+{
+	struct mlk_sound *snd = mlk_js_sound_this(ctx);
+
+	if (mlk_sound_play(snd) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sound_pause(duk_context *ctx)
+{
+	mlk_sound_pause(mlk_js_sound_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sound_resume(duk_context *ctx)
+{
+	mlk_sound_resume(mlk_js_sound_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sound_stop(duk_context *ctx)
+{
+	mlk_sound_stop(mlk_js_sound_this(ctx));
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sound_finish(duk_context *ctx)
+{
+	struct mlk_sound *snd;
+
+	duk_get_prop_string(ctx, 0, SYMBOL);
+
+	if ((snd = duk_to_pointer(ctx, -1))) {
+		mlk_sound_finish(snd);
+		mlk_alloc_free(snd);
+	}
+
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+
+	return 0;
+}
+
+static const duk_function_list_entry methods[] = {
+	{ "play",               mlk_js_sound_play,      DUK_VARARGS     },
+	{ "pause",              mlk_js_sound_pause,     0               },
+	{ "resume",             mlk_js_sound_resume,    0               },
+	{ "stop",               mlk_js_sound_stop,      DUK_VARARGS     },
+	{ NULL,                 NULL,                   0               }
+};
+
+void
+mlk_js_sound_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_sound_new, 3);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_sound_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Sound");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-sound.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-sound.h -- sound support (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_SOUND_H
+#define MLK_CORE_JS_SOUND_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_sound_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_SOUND_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-sprite.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,178 @@
+/*
+ * js-sprite.c -- image sprites (Javascript bindings)
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/sprite.h>
+#include <mlk/core/texture.h>
+
+#include "js-sprite.h"
+#include "js-texture.h"
+#include "js.h"
+
+#define SYMBOL          DUK_HIDDEN_SYMBOL("mlk::sprite")
+#define TEXTURE_REF     DUK_HIDDEN_SYMBOL("mlk::sprite::texture")
+
+static struct mlk_sprite *
+mlk_js_sprite_this(duk_context *ctx)
+{
+	struct mlk_sprite *sprite;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	sprite = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!sprite)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a Sprite object");
+
+	return sprite;
+}
+
+static duk_ret_t
+mlk_js_sprite_getRowCount(duk_context *ctx)
+{
+	duk_push_uint(ctx, mlk_js_sprite_this(ctx)->nrows);
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_sprite_getColumnCount(duk_context *ctx)
+{
+	duk_push_uint(ctx, mlk_js_sprite_this(ctx)->ncols);
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_sprite_new(duk_context *ctx)
+{
+	struct mlk_sprite sprite;
+
+	if (!duk_is_constructor_call(ctx))
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Sprite must be new-constructed");
+
+	sprite.texture = mlk_js_texture_require(ctx, 0);
+	sprite.cellw   = duk_require_int(ctx, 1);
+	sprite.cellh   = duk_require_int(ctx, 2);
+	mlk_sprite_init(&sprite);
+
+	duk_push_this(ctx);
+	duk_push_pointer(ctx, mlk_alloc_dup(&sprite, 1, sizeof (sprite)));
+	duk_put_prop_string(ctx, -2, SYMBOL);
+
+	/*
+	 * Put rowCount/columnCount properties.
+	 */
+	duk_push_string(ctx, "rowCount");
+	duk_push_c_function(ctx, mlk_js_sprite_getRowCount, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+
+	duk_push_string(ctx, "columnCount");
+	duk_push_c_function(ctx, mlk_js_sprite_getColumnCount, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+
+	/* We store the texture into the sprite to avoid being collected. */
+	duk_dup(ctx, 0);
+	duk_put_prop_string(ctx, -2, TEXTURE_REF);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sprite_finish(duk_context *ctx)
+{
+	duk_get_prop_string(ctx, 0, SYMBOL);
+	free(duk_to_pointer(ctx, -1));
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+	duk_del_prop_string(ctx, 0, TEXTURE_REF);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_sprite_draw(duk_context *ctx)
+{
+	struct sprite *sprite = mlk_js_sprite_this(ctx);
+	unsigned int r, c;
+	int x, y;
+
+	if (duk_get_top(ctx) == 4) {
+		r = duk_require_uint(ctx, 0);
+		c = duk_require_uint(ctx, 1);
+		x = duk_require_int(ctx, 2);
+		y = duk_require_int(ctx, 3);
+	} else if (duk_get_top(ctx) == 1) {
+		duk_require_object(ctx, 0);
+		duk_get_prop_string(ctx, 0, "row");
+		r = duk_to_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "column");
+		c = duk_to_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "x");
+		x = duk_to_int(ctx, -1);
+		duk_get_prop_string(ctx, 0, "y");
+		y = duk_to_int(ctx, -1);
+		duk_pop_n(ctx, 4);
+	} else
+		return duk_error(ctx, DUK_ERR_ERROR, "Object or 4 numbers expected");
+
+	mlk_sprite_draw(sprite, r, c, x, y);
+
+	return 0;
+}
+
+static const duk_function_list_entry methods[] = {
+	{ "draw",       mlk_js_sprite_draw,     DUK_VARARGS     },
+	{ NULL,         NULL,                   0               }
+};
+
+struct mlk_sprite *
+mlk_js_sprite_require(duk_context *ctx, duk_idx_t index)
+{
+	struct sprite *sp;
+
+	duk_get_prop_string(ctx, index, SYMBOL);
+	sp = duk_to_pointer(ctx, -1);
+	duk_pop(ctx);
+
+	if (!sp)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Sprite expected on argument #%u", index);
+
+	return sp;
+}
+
+void
+mlk_js_sprite_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Molko");
+	duk_push_c_function(ctx, mlk_js_sprite_new, 3);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_sprite_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Sprite");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-sprite.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,40 @@
+/*
+ * js-sprite.h -- image sprites (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_SPRITE_H
+#define MLK_CORE_JS_SPRITE_H
+
+#include <duktape.h>
+
+struct mlk_sprite;
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_sprite_load(duk_context *ctx);
+
+struct mlk_sprite *
+mlk_js_sprite_require(duk_context *ctx, duk_idx_t index);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_SPRITE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-texture.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,294 @@
+/*
+ * js-texture.c -- basic texture management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/alloc.h>
+#include <mlk/core/err.h>
+#include <mlk/core/image.h>
+#include <mlk/core/texture.h>
+
+#include "js-texture.h"
+#include "js.h"
+
+#define SYMBOL DUK_HIDDEN_SYMBOL("mlk::texture")
+#define PROTO  DUK_HIDDEN_SYMBOL("mlk::texture::proto")
+
+static struct mlk_texture *
+mlk_js_texture_this(duk_context *ctx)
+{
+	struct mlk_texture *tex;
+
+	duk_push_this(ctx);
+	duk_get_prop_string(ctx, -1, SYMBOL);
+	tex = duk_to_pointer(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (!tex)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Not a Texture object");
+
+	return tex;
+}
+
+static duk_ret_t
+mlk_js_texture_getWidth(duk_context *ctx)
+{
+	duk_push_uint(ctx, mlk_js_texture_this(ctx)->w);
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_texture_getHeight(duk_context *ctx)
+{
+	duk_push_uint(ctx, mlk_js_texture_this(ctx)->h);
+
+	return 1;
+}
+
+static duk_ret_t
+mlk_js_texture_setBlendMode(duk_context *ctx)
+{
+	struct mlk_texture *tex = mlk_js_texture_this(ctx);
+	enum mlk_texture_blend blend = duk_require_int(ctx, 0);
+
+	if (blend < 0 || blend >= MLK_TEXTURE_BLEND_MODULATE)
+		return duk_error(ctx, DUK_ERR_ERROR, "invalid blend mode: %d", blend);
+	if (mlk_texture_set_blend_mode(tex, blend) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_setAlphaMod(duk_context *ctx)
+{
+	struct mlk_texture *tex = mlk_js_texture_this(ctx);
+	unsigned int alpha = duk_require_uint(ctx, 0);
+
+	if (alpha > 255)
+		return duk_error(ctx, DUK_ERR_RANGE_ERROR, "%u is out of range [0..255]", alpha);
+	if (mlk_texture_set_alpha_mod(tex, alpha) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_setColorMod(duk_context *ctx)
+{
+	struct mlk_texture *tex = mlk_js_texture_this(ctx);
+	unsigned long color = duk_require_number(ctx, 0);
+
+	if (mlk_texture_set_color_mod(tex, color) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_new(duk_context *ctx)
+{
+	struct mlk_texture tex;
+	unsigned int w = duk_require_uint(ctx, 0);
+	unsigned int h = duk_require_uint(ctx, 1);
+
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Texture must be new-constructed");
+	if (mlk_texture_init(&tex, w, h) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	duk_push_this(ctx);
+	duk_push_pointer(ctx, mlk_alloc_dup(&tex, 1, sizeof (tex)));
+	duk_put_prop_string(ctx, -2, SYMBOL);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_draw(duk_context *ctx)
+{
+	struct mlk_texture *tex = mlk_js_texture_this(ctx);
+	int x, y;
+
+	duk_require_object(ctx, 0);
+	duk_get_prop_string(ctx, 0, "x");
+	x = duk_to_int(ctx, -1);
+	duk_get_prop_string(ctx, 0, "y");
+	y = duk_to_int(ctx, -1);
+	duk_pop_n(ctx, 2);
+
+	if (mlk_texture_draw(tex, x, y) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_scale(duk_context *ctx)
+{
+	struct mlk_texture *tex = mlk_js_texture_this(ctx);
+	int srcx, srcy, dstx, dsty;
+	unsigned int srcw, srch, dstw, dsth;
+	double angle;
+
+	/* Source rectangle. */
+	duk_require_object(ctx, 0);
+	duk_get_prop_string(ctx, 0, "x");
+	srcx = duk_to_int(ctx, -1);
+	duk_get_prop_string(ctx, 0, "y");
+	srcy = duk_to_int(ctx, -1);
+	duk_get_prop_string(ctx, 0, "w");
+	srcw = duk_is_number(ctx, -1) ? duk_to_uint(ctx, -1) : tex->w;
+	duk_get_prop_string(ctx, 0, "h");
+	srch = duk_is_number(ctx, -1) ? duk_to_uint(ctx, -1) : tex->h;
+	duk_pop_n(ctx, 4);
+
+	/* Destination rectangle. */
+	duk_require_object(ctx, 1);
+	duk_get_prop_string(ctx, 1, "x");
+	dstx = duk_to_int(ctx, -1);
+	duk_get_prop_string(ctx, 1, "y");
+	dsty = duk_to_int(ctx, -1);
+	duk_get_prop_string(ctx, 1, "w");
+	dstw = duk_is_number(ctx, -1) ? duk_to_uint(ctx, -1) : tex->w;
+	duk_get_prop_string(ctx, 1, "h");
+	dsth = duk_is_number(ctx, -1) ? duk_to_uint(ctx, -1) : tex->h;
+	duk_pop_n(ctx, 4);
+
+	/* Angle. */
+	angle = duk_to_number(ctx, 2);
+
+	if (mlk_texture_scale(tex, srcx, srcy, srcw, srch, dstx, dsty, dstw, dsth, angle) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_finish(duk_context *ctx)
+{
+	struct mlk_texture *tex;
+
+	duk_get_prop_string(ctx, 0, SYMBOL);
+	tex = duk_to_pointer(ctx, -1);
+	duk_pop(ctx);
+	duk_del_prop_string(ctx, 0, SYMBOL);
+
+	if (tex) {
+		mlk_texture_finish(tex);
+		mlk_alloc_free(tex);
+	}
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_texture_fromImage(duk_context *ctx)
+{
+	const char *path = duk_require_string(ctx, 0);
+	struct mlk_texture tex;
+
+	if (mlk_image_open(&tex, path) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	mlk_js_texture_push(ctx, &tex);
+
+	return 1;
+}
+
+static const duk_function_list_entry methods[] = {
+	{ "draw",       mlk_js_texture_draw,            1 },
+	{ "scale",      mlk_js_texture_scale,           3 },
+	{ NULL,         NULL,                           0 }
+};
+
+static const duk_function_list_entry functions[] = {
+	{ "fromImage",  mlk_js_texture_fromImage,       1 },
+	{ NULL,         NULL,                           0 }
+};
+
+struct mlk_texture *
+mlk_js_texture_require(duk_context *ctx, duk_idx_t index)
+{
+	struct mlk_texture *tex;
+
+	duk_get_prop_string(ctx, index, SYMBOL);
+	tex = duk_to_pointer(ctx, -1);
+	duk_pop(ctx);
+
+	if (!tex)
+		duk_error(ctx, DUK_ERR_TYPE_ERROR, "Texture expected on argument #%u", index);
+
+	return tex;
+}
+
+void
+mlk_js_texture_push(duk_context *ctx, const struct mlk_texture *tex)
+{
+	assert(ctx);
+	assert(tex);
+
+	duk_push_object(ctx);
+	duk_push_global_stash(ctx);
+	duk_get_prop_string(ctx, -1, PROTO);
+	duk_remove(ctx, -2);
+	duk_push_pointer(ctx, mlk_alloc_dup(tex, 1, sizeof (*tex)));
+	duk_put_prop_string(ctx, -3, SYMBOL);
+	duk_set_prototype(ctx, -2);
+
+	/* Put some properties. */
+	duk_push_string(ctx, "width");
+	duk_push_c_function(ctx, mlk_js_texture_getWidth, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+	duk_push_string(ctx, "height");
+	duk_push_c_function(ctx, mlk_js_texture_getHeight, 0);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_GETTER);
+	duk_push_string(ctx, "blendMode");
+	duk_push_c_function(ctx, mlk_js_texture_setBlendMode, 1);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_SETTER);
+	duk_push_string(ctx, "alphaMod");
+	duk_push_c_function(ctx, mlk_js_texture_setAlphaMod, 1);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_SETTER);
+	duk_push_string(ctx, "colorMod");
+	duk_push_c_function(ctx, mlk_js_texture_setColorMod, 1);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_SETTER);
+}
+
+void
+mlk_js_texture_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_texture_new, 2);
+	duk_put_function_list(ctx, -1, functions);
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, methods);
+	duk_push_c_function(ctx, mlk_js_texture_finish, 1);
+	duk_set_finalizer(ctx, -2);
+	duk_push_global_stash(ctx);
+	duk_dup(ctx, -2);
+	duk_put_prop_string(ctx, -2, PROTO);
+	duk_pop(ctx);
+	duk_put_prop_string(ctx, -2, "prototype");
+	duk_put_prop_string(ctx, -2, "Texture");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-texture.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,43 @@
+/*
+ * js-texture.h -- basic texture management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_TEXTURE_H
+#define MLK_CORE_JS_TEXTURE_H
+
+#include <duktape.h>
+
+struct mlk_texture;
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+struct mlk_texture *
+mlk_js_texture_require(duk_context *ctx, duk_idx_t index);
+
+void
+mlk_js_texture_push(duk_context *ctx, const struct mlk_texture *tex);
+
+void
+mlk_js_texture_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_TEXTURE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-util.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,50 @@
+/*
+ * js-util.c -- utilities (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/util.h>
+
+#include "js-util.h"
+#include "js.h"
+
+static duk_ret_t
+mlk_js_util_sleep(duk_context *ctx)
+{
+	mlk_util_sleep(duk_require_uint(ctx, 0));
+
+	return 0;
+}
+
+static const duk_function_list_entry functions[] = {
+	{ "sleep",      mlk_js_util_sleep,      1 },
+	{ NULL,         NULL,                   0 }
+};
+
+void
+mlk_js_util_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_object(ctx);
+	duk_put_function_list(ctx, -1, functions);
+	duk_put_prop_string(ctx, -2, "Util");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-util.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-util.h -- utilities (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_UTIL_H
+#define MLK_CORE_JS_UTIL_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_util_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_UTIL_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-window.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,85 @@
+/*
+ * js-window.c -- basic window management (Javascript bindings)
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+
+#include <mlk/core/err.h>
+#include <mlk/core/window.h>
+
+#include "js-window.h"
+#include "js.h"
+
+static duk_ret_t
+mlk_js_window_setCursor(duk_context *ctx)
+{
+	enum mlk_window_cursor cursor = duk_require_int(ctx, 0);
+
+	if (cursor < 0 || cursor > MLK_WINDOW_CURSOR_LAST)
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "invalid cursor type: %d", cursor);
+
+	mlk_window_set_cursor(cursor);
+
+	return 0;
+}
+
+static duk_ret_t
+mlk_js_window_new(duk_context *ctx)
+{
+	const char *str = duk_require_string(ctx, 0);
+	unsigned int w = duk_require_int(ctx, 1);
+	unsigned int h = duk_require_int(ctx, 2);
+
+	if (!duk_is_constructor_call(ctx))
+		return duk_error(ctx, DUK_ERR_TYPE_ERROR, "Window must be new-constructed");
+	if (mlk_window_open(str, w, h) < 0)
+		duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+
+	duk_push_this(ctx);
+	duk_push_string(ctx, "cursor");
+	duk_push_c_function(ctx, mlk_js_window_setCursor, 1);
+	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_SETTER);
+	duk_pop(ctx);
+
+	return 0;
+}
+
+static const duk_number_list_entry cursors[] = {
+	{ "ARROW",      MLK_WINDOW_CURSOR_ARROW         },
+	{ "EDIT",       MLK_WINDOW_CURSOR_EDIT          },
+	{ "WAIT",       MLK_WINDOW_CURSOR_WAIT          },
+	{ "CROSS_HAIR", MLK_WINDOW_CURSOR_CROSSHAIR     },
+	{ "SIZE",       MLK_WINDOW_CURSOR_SIZE          },
+	{ "NO",         MLK_WINDOW_CURSOR_NO            },
+	{ "HAND",       MLK_WINDOW_CURSOR_HAND          },
+	{ NULL,         0                               }
+};
+
+void
+mlk_js_window_load(duk_context *ctx)
+{
+	assert(ctx);
+
+	duk_push_global_object(ctx);
+	duk_get_prop_string(ctx, -1, "Mlk");
+	duk_push_c_function(ctx, mlk_js_window_new, 3);
+	duk_push_object(ctx);
+	duk_put_number_list(ctx, -1, cursors);
+	duk_put_prop_string(ctx, -2, "Cursor");
+	duk_put_prop_string(ctx, -2, "Window");
+	duk_pop_n(ctx, 2);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/js-window.h	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,35 @@
+/*
+ * js-window.h -- basic window management (Javascript bindings)
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef MLK_CORE_JS_WINDOW_H
+#define MLK_CORE_JS_WINDOW_H
+
+#include <duktape.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+void
+mlk_js_window_load(duk_context *ctx);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif /* !MLK_CORE_JS_WINDOW_H */
--- a/libmlk-core/mlk/core/js/js.c	Sun Oct 01 09:18:01 2023 +0200
+++ b/libmlk-core/mlk/core/js/js.c	Sun Dec 17 09:50:36 2023 +0100
@@ -137,6 +137,7 @@
 	duk_pop(js->handle);
 }
 
+#if 0
 static void
 setup_base(struct mlk_js *js, const char *path)
 {
@@ -150,6 +151,7 @@
 	duk_put_prop_string(js->handle, -2, DUK_HIDDEN_SYMBOL("mlk::base"));
 	duk_pop(js->handle);
 }
+#endif
 
 int
 js_init(struct mlk_js *js)
--- a/libmlk-core/mlk/core/js/js.h	Sun Oct 01 09:18:01 2023 +0200
+++ b/libmlk-core/mlk/core/js/js.h	Sun Dec 17 09:50:36 2023 +0100
@@ -23,10 +23,18 @@
 	void *handle;
 };
 
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
 int
 mlk_js_init(struct mlk_js *js);
 
 void
 mlk_js_finish(struct mlk_js *js);
 
+#if defined(__cplusplus)
+}
+#endif
+
 #endif /* !MLK_CORE_JS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libmlk-core/mlk/core/js/main.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,165 @@
+/*
+ * main.c -- main Javascript loader
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <setjmp.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+
+#include <core/alloc.h>
+#include <core/core.h>
+#include <core/error.h>
+#include <core/event.h>
+#include <core/painter.h>
+#include <core/panic.h>
+#include <core/window.h>
+
+#include <ui/label.h>
+#include <ui/ui.h>
+
+#include <rpg/rpg.h>
+
+#include "js.h"
+
+static jmp_buf panic_buf;
+
+static noreturn void
+jump(void)
+{
+	longjmp(panic_buf, 1);
+}
+
+static noreturn void
+usage(void)
+{
+	fprintf(stderr, "usage: molko-js main.js\n");
+	exit(1);
+}
+
+static noreturn void
+die(void)
+{
+	fprintf(stderr, "abort: %s\n", error());
+	exit(1);
+}
+
+static void
+init(void)
+{
+	if (!core_init() || !ui_init() || !rpg_init())
+		panic();
+
+	panic_handler = jump;
+}
+
+static void
+draw_info(void)
+{
+	struct label label = {
+		.text = "An unexpected error happened, game must stops.",
+		.x = 10,
+		.y = 10,
+		.flags = LABEL_FLAGS_SHADOW
+	};
+
+	label_draw(&label);
+}
+
+static void
+draw_error(void)
+{
+	struct label label = {
+		.text = error(),
+		.x = 10,
+		.y = 40,
+		.flags = LABEL_FLAGS_SHADOW
+	};
+
+	label_draw(&label);
+}
+
+static void
+run_panic(void)
+{
+	panic_handler = die;
+
+	for (;;) {
+		union event ev;
+
+		while (event_poll(&ev)) {
+			switch (ev.type) {
+			case EVENT_QUIT:
+				return;
+			default:
+				break;
+			}
+		}
+
+		painter_set_color(0x752438ff);
+		painter_clear();
+		draw_info();
+		draw_error();
+		painter_present();
+	}
+}
+
+static void
+run(const char *path)
+{
+	struct js js;
+
+	if (setjmp(panic_buf) == 0) {
+		if (!js_init(&js))
+			panic();
+
+		js_core_load(&js);
+
+		if (!js_open(&js, path))
+			panic();
+	} else {
+		/* TODO: change once we have window_ok function. */
+		if (window.w && window.h)
+			run_panic();
+		else
+			die();
+	}
+}
+
+static void
+quit(void)
+{
+	rpg_finish();
+	ui_finish();
+	core_finish();
+}
+
+int
+main(int argc, char **argv)
+{
+	--argc;
+	++argv;
+
+	if (argc == 0)
+		usage();
+
+	init();
+	run(argv[0]);
+	quit();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mlk-run/CMakeLists.txt	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,34 @@
+#
+# CMakeLists.txt -- CMake build system for Molko's Engine
+#
+# Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+project(mlk-run)
+
+set(
+	SOURCES
+	${mlk-run_SOURCE_DIR}/mlk-run.c
+)
+
+mlk_executable(
+	NAME mlk-run
+	SOURCES ${SOURCES}
+	LIBRARIES libmlk-core-js
+	INSTALL
+	FOLDER tools
+)
+
+add_executable(mlk::mlk-run ALIAS mlk-run)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mlk-run/mlk-run.c	Sun Dec 17 09:50:36 2023 +0100
@@ -0,0 +1,197 @@
+/*
+ * main.c -- main mlk-run file
+ *
+ * Copyright (c) 2020-2023 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+
+#include <duktape.h>
+#include <duk_module.h>
+
+#include <mlk/util/util.h>
+
+#include <mlk/core/core.h>
+#include <mlk/core/err.h>
+#include <mlk/core/panic.h>
+#include <mlk/core/vfs-dir.h>
+#include <mlk/core/vfs-zip.h>
+#include <mlk/core/vfs.h>
+
+union {
+	struct mlk_vfs_dir dir;
+
+#if defined(MLK_WITH_ZIP)
+	struct mlk_vfs_zip zip;
+#endif
+} iface;
+
+/* VFS loader to support zip and directories when loading game. */
+static struct mlk_vfs *vfs;
+
+/* Javascript context. */
+static duk_context *ctx;
+
+static duk_ret_t
+modsearch(duk_context *ctx)
+{
+	char path[MLK_PATH_MAX] = {}, *data;
+	struct mlk_vfs_file *file;
+	size_t datasz;
+
+	snprintf(path, sizeof (path), "%s.js", duk_require_string(ctx, 0));
+
+	if ((file = mlk_vfs_open(vfs, path, "r")) < 0)
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+	if (!(data = mlk_vfs_file_read_all(file, &datasz))) {
+		mlk_vfs_file_finish(file);
+		return duk_error(ctx, DUK_ERR_ERROR, "%s", mlk_err());
+	}
+
+	mlk_vfs_file_finish(file);
+	duk_push_lstring(ctx, data, datasz);
+	free(data);
+
+	return 1;
+}
+
+static void
+core_bind(duk_context *ctx)
+{
+#if 0
+	/* Brings Mlk global object. */
+	mlk_js_core_bind(ctx, &vfs);
+
+	js_action_bind(ctx);
+	js_action_stack_bind(ctx);
+	js_animation_bind(ctx);
+	js_clock_bind(ctx);
+	js_color_bind(ctx);
+	js_drawable_bind(ctx);
+	js_drawable_stack_bind(ctx);
+	js_event_bind(ctx);
+	js_font_bind(ctx);
+	js_game_bind(ctx);
+	js_music_bind(ctx);
+	js_painter_bind(ctx);
+	js_panic_bind(ctx);
+	js_sound_bind(ctx);
+	js_sprite_bind(ctx);
+	js_state_bind(ctx);
+	js_texture_bind(ctx);
+	js_trace_bind(ctx);
+	js_window_bind(ctx);
+#endif
+}
+
+static void
+init(void)
+{
+	/* TODO: this is temporary. */
+	if (mlk_core_init("fr.malikania", "mlk-run") < 0)
+		mlk_panic();
+
+	/* Fireup Javascript. */
+	ctx = duk_create_heap_default();
+	core_bind(ctx);
+
+	/* Setup module loader. */
+	duk_module_duktape_init(ctx);
+	duk_get_global_string(ctx, "Duktape");
+	duk_push_c_function(ctx, modsearch, 4);
+	duk_put_prop_string(ctx, -2, "modSearch");
+	duk_pop(ctx);
+}
+
+static void
+startup(void)
+{
+	struct mlk_vfs_file *file;
+	char *code;
+
+	if (!(file = mlk_vfs_open(vfs, "main.js", "r")))
+		mlk_panic();
+	if (!(code = mlk_vfs_file_read_all(file, NULL))) {
+		mlk_vfs_file_finish(file);
+		mlk_panic();
+	}
+
+	mlk_vfs_file_finish(file);
+
+	if (duk_peval_string(ctx, code))
+		mlk_panicf("%s", duk_safe_to_string(ctx, -1));
+
+	free(code);
+}
+
+static void
+finish(void)
+{
+	mlk_vfs_finish(vfs);
+	duk_destroy_heap(ctx);
+}
+
+static void
+loadzip(const char *path)
+{
+#if 0
+	if (vfs_zip(&vfs, path, "r") < 0)
+		panic();
+#endif
+}
+
+static void
+loaddirectory(const char *path)
+{
+#if 0
+	vfs_directory(&vfs, path);
+#endif
+}
+
+static void
+load(const char *path)
+{
+	/* TODO: improve this. */
+	if (strstr(path, ".mlk"))
+		loadzip(path);
+	else
+		loaddirectory(path);
+
+	startup();
+}
+
+static void
+run(int argc, char **argv)
+{
+	if (argc < 0) {
+		fprintf(stderr, "usage: mlk-run game\n");
+		exit(1);
+	}
+
+	load(argv[0]);
+}
+
+int
+main(int argc, char **argv)
+{
+	--argc;
+	++argv;
+
+	init();
+	run(argc, argv);
+	finish();
+}