add initial client code

Sun, 30 Nov 2025 18:15:46 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 30 Nov 2025 18:15:46 +0100
changeset 942
488178e3e328
parent 941
e7459e9fbed2
child 943
9b5948aa5b90

add initial client code

client/Makefile file | annotate | diff | comparison | revisions
client/Makefile.unix file | annotate | diff | comparison | revisions
client/Makefile.win32 file | annotate | diff | comparison | revisions
client/app.manifest file | annotate | diff | comparison | revisions
client/app.rc file | annotate | diff | comparison | revisions
client/args.c file | annotate | diff | comparison | revisions
client/args.h file | annotate | diff | comparison | revisions
client/main.c file | annotate | diff | comparison | revisions
client/main.h file | annotate | diff | comparison | revisions
client/message.c file | annotate | diff | comparison | revisions
client/message.h file | annotate | diff | comparison | revisions
client/uiclient.c file | annotate | diff | comparison | revisions
client/uiclient.h file | annotate | diff | comparison | revisions
configure file | annotate | diff | comparison | revisions
make/Makefile.mk file | annotate | diff | comparison | revisions
make/project.xml file | annotate | diff | comparison | revisions
make/toolchain.sh file | annotate | diff | comparison | revisions
ui/common/args.h file | annotate | diff | comparison | revisions
ui/gtk/toolkit.c file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/Makefile	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,57 @@
+#
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+#
+# Copyright 2011 Olaf Wintermann. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#   1. Redistributions of source code must retain the above copyright notice,
+#      this list of conditions and the following disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+BUILD_ROOT = ..
+include ../config.mk
+
+CFLAGS += -I../ui/ -I../ucx
+
+APP_BIN_OBJ = ../build/client/main$(OBJ_EXT)
+APP_BIN_OBJ += ../build/client/message$(OBJ_EXT)
+APP_BIN_OBJ += ../build/client/uiclient$(OBJ_EXT)
+APP_BIN_OBJ += ../build/client/args$(OBJ_EXT)
+
+APP_BIN = ../build/$(BUILD_BIN_DIR)/ui-client$(APP_EXT)
+
+all:
+	@if test -z "$(DISABLE_CLIENT)"; then \
+		$(MAKE) client; \
+	else \
+		echo "ui-client disabled"; \
+	fi
+
+include $(SYS_MAKEFILE)
+
+client: $(APP_BIN)
+
+$(APP_BIN): $(APP_BIN_OBJ) $(RES_FILE) $(BUILD_ROOT)/build/$(BUILD_LIB_DIR)/$(LIB_PREFIX)uitk$(LIB_EXT)
+	$(LD) -o $(APP_BIN) $(APP_BIN_OBJ) $(RES_FILE) $(BUILD_ROOT)/build/$(BUILD_LIB_DIR)/$(LIB_PREFIX)uitk$(LIB_EXT) $(BUILD_ROOT)/build/$(BUILD_LIB_DIR)/$(LIB_PREFIX)ucx$(LIB_EXT) $(LDFLAGS) $(TK_LDFLAGS) $(CLIENT_LDFLAGS)
+
+../build/client/%$(OBJ_EXT): %.c
+	$(CC) $(CFLAGS) $(TK_CFLAGS) $(CLIENT_CFLAGS) -o $@ -c $<
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/Makefile.win32	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,5 @@
+RES_FILE = app.res
+
+$(RES_FILE): app.rc app.manifest
+	llvm-rc app.rc
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/app.manifest	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+        type="win32"
+        name="Microsoft.Windows.Common-Controls"
+        version="6.0.0.0"
+        processorArchitecture="*"
+        publicKeyToken="6595b64144ccf1df"
+        language="*"
+      />
+    </dependentAssembly>
+  </dependency>
+</assembly>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/app.rc	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,1 @@
+1 24 "app.manifest"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/args.c	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,234 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "args.h"
+
+#include "../ui/common/args.h"
+
+#define DEFAULT_ARG_FUNCS(var, prefix) \
+    static ArgDefaultFuncs var = { \
+        .fill = (argfunc_set_bool)prefix##_args_set_fill, \
+        .hexpand = (argfunc_set_bool)prefix##_args_set_hexpand, \
+        .vexpand = (argfunc_set_bool)prefix##_args_set_vexpand, \
+        .hfill = (argfunc_set_bool)prefix##_args_set_hfill, \
+        .vfill = (argfunc_set_bool)prefix##_args_set_vfill, \
+        .override_defaults = (argfunc_set_bool)prefix##_args_set_override_defaults, \
+        .margin = (argfunc_set_int)prefix##_args_set_margin, \
+        .margin_left = (argfunc_set_int)prefix##_args_set_margin_left, \
+        .margin_right = (argfunc_set_int)prefix##_args_set_margin_right, \
+        .margin_top = (argfunc_set_int)prefix##_args_set_margin_top, \
+        .margin_bottom = (argfunc_set_int)prefix##_args_set_margin_bottom, \
+        .colspan = (argfunc_set_int)prefix##_args_set_colspan, \
+        .rowspan = (argfunc_set_int)prefix##_args_set_rowspan, \
+        .name = (argfunc_set_str)prefix##_args_set_name, \
+        .style_class = (argfunc_set_str)prefix##_args_set_style_class  \
+    } 
+
+DEFAULT_ARG_FUNCS(container_args, ui_container);
+
+DEFAULT_ARG_FUNCS(button_args, ui_button);
+
+
+
+static void init_common_args(const CxJsonValue *value, void *args, ArgDefaultFuncs *funcs) {
+    CxJsonValue *val;
+    
+    // boolean args
+    val = cxJsonObjGet(value, "fill");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        funcs->fill(args, TRUE);
+    }
+    
+    val = cxJsonObjGet(value, "hexpand");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        funcs->hexpand(args, TRUE);
+    }
+    
+    val = cxJsonObjGet(value, "vexpand");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        funcs->vexpand(args, TRUE);
+    }
+    
+    val = cxJsonObjGet(value, "hfill");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        funcs->hfill(args, TRUE);
+    }
+    
+    val = cxJsonObjGet(value, "vfill");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        funcs->vfill(args, TRUE);
+    }
+    
+    val = cxJsonObjGet(value, "override_defaults");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        funcs->override_defaults(args, TRUE);
+    }
+    
+    // int args
+    val = cxJsonObjGet(value, "margin");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->margin(args, (int)val->value.integer);
+    }
+    
+    val = cxJsonObjGet(value, "margin_left");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->margin_left(args, (int)val->value.integer);
+    }
+    
+    val = cxJsonObjGet(value, "margin_right");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->margin_right(args, (int)val->value.integer);
+    }
+    
+    val = cxJsonObjGet(value, "margin_top");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->margin_top(args, (int)val->value.integer);
+    }
+    
+    val = cxJsonObjGet(value, "margin_bottom");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->margin_bottom(args, (int)val->value.integer);
+    }
+    
+    val = cxJsonObjGet(value, "colspan");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->colspan(args, (int)val->value.integer);
+    }
+    
+    val = cxJsonObjGet(value, "rowspan");
+    if(val && val->type == CX_JSON_INTEGER) {
+        funcs->rowspan(args, (int)val->value.integer);
+    }
+    
+    // string args
+    val = cxJsonObjGet(value, "name");
+    if(val && val->type == CX_JSON_STRING) {
+        funcs->name(args, val->value.string.ptr);
+    }
+    
+    val = cxJsonObjGet(value, "style_class");
+    if(val && val->type == CX_JSON_STRING) {
+        funcs->name(args, val->value.string.ptr);
+    }
+}
+
+void init_groups(const CxJsonValue *value, void *args, argfunc_set_intarray setarray) {
+    CxJsonValue *val = cxJsonObjGet(value, "states");
+    if(!val || val->type != CX_JSON_ARRAY) {
+        return;
+    }
+    
+    int len = (int)val->value.array.array_size;
+    int *states = calloc(len, sizeof(int));
+    for(int i=0;i<len;i++) {
+        CxJsonValue *s = val->value.array.array[i];
+        if(s->type == CX_JSON_INTEGER) {
+            states[i] = (int)s->value.integer;
+        }
+    }
+    
+    setarray(args, states, len);
+}
+
+UiContainerArgs* json2container_args(const CxJsonValue *value) {
+    UiContainerArgs *args = ui_container_args_new();
+    if(value->type != CX_JSON_OBJECT) {
+        return args;
+    }
+    
+    init_common_args(value, args, &container_args);
+    
+    CxJsonValue *val = cxJsonObjGet(value, "spacing");
+    if(val && val->type == CX_JSON_INTEGER) {
+        args->spacing = (int)val->value.integer;
+    }
+    
+    val = cxJsonObjGet(value, "columnspacing");
+    if(val && val->type == CX_JSON_INTEGER) {
+        args->columnspacing = (int)val->value.integer;
+    }
+    
+    val = cxJsonObjGet(value, "rowspacing");
+    if(val && val->type == CX_JSON_INTEGER) {
+        args->rowspacing = (int)val->value.integer;
+    }
+    
+    val = cxJsonObjGet(value, "def_hfill");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        args->def_hfill = TRUE;
+    }
+    
+    val = cxJsonObjGet(value, "def_vfill");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        args->def_vfill = TRUE;
+    }
+    
+    val = cxJsonObjGet(value, "def_hexpand");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        args->def_hexpand = TRUE;
+    }
+    
+    val = cxJsonObjGet(value, "def_vexpand");
+    if(val && val->type == CX_JSON_LITERAL && val->value.literal == CX_JSON_TRUE) {
+        args->def_vexpand = TRUE;
+    }
+    
+    return args;
+}
+
+UiButtonArgs* json2button_args(const CxJsonValue *value) {
+    UiButtonArgs *args = ui_button_args_new();
+    if(value->type != CX_JSON_OBJECT) {
+        return args;
+    }
+    
+    init_common_args(value, args, &button_args);
+    init_groups(value, args, (argfunc_set_intarray)ui_button_args_set_groups);
+    
+    CxJsonValue *val = cxJsonObjGet(value, "label");
+    if(val && val->type == CX_JSON_STRING) {
+        ui_button_args_set_label(args, val->value.string.ptr);
+    }
+    
+    val = cxJsonObjGet(value, "icon");
+    if(val && val->type == CX_JSON_STRING) {
+        ui_button_args_set_icon(args, val->value.string.ptr);
+    }
+    
+    val = cxJsonObjGet(value, "tooltip");
+    if(val && val->type == CX_JSON_STRING) {
+        ui_button_args_set_tooltip(args, val->value.string.ptr);
+    }
+    
+    val = cxJsonObjGet(value, "labeltype");
+    if(val && val->type == CX_JSON_INTEGER) {
+        ui_button_args_set_labeltype(args, (int)val->value.integer);
+    }
+    
+    return args;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/args.h	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,73 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CLIENT_ARGS_H
+#define CLIENT_ARGS_H
+
+#include <ui/ui.h>
+#include <cx/json.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+    
+typedef void(*argfunc_set_bool)(void *, UiBool);
+typedef void(*argfunc_set_int)(void *, int);
+typedef void(*argfunc_set_str)(void *, const char*);
+typedef void(*argfunc_set_intarray)(void *, int *, int);
+    
+typedef struct ArgDefaultFuncs {
+    argfunc_set_bool fill;
+    argfunc_set_bool hexpand;
+    argfunc_set_bool vexpand;
+    argfunc_set_bool hfill;
+    argfunc_set_bool vfill;
+    argfunc_set_bool override_defaults;
+    argfunc_set_int  margin;
+    argfunc_set_int  margin_left;
+    argfunc_set_int  margin_right;
+    argfunc_set_int  margin_top;
+    argfunc_set_int  margin_bottom;
+    argfunc_set_int  colspan;
+    argfunc_set_int  rowspan;
+    argfunc_set_str  name;
+    argfunc_set_str  style_class;
+    
+} ArgDefaultFuncs;
+
+UiContainerArgs* json2container_args(const CxJsonValue *value);
+
+UiButtonArgs* json2button_args(const CxJsonValue *value);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CLIENT_ARGS_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/main.c	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,204 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <ui/ui.h>
+#include <cx/printf.h>
+#include <pthread.h>
+
+#include "main.h"
+#include "uiclient.h"
+
+/*
+ * debug window that is always created
+ */
+static UiObject *debug_window;
+
+/*
+ * test window, that is only created in testing mode
+ */
+static UiObject *test_window;
+
+static int test_mode = 0;
+
+static int input_fd[2];
+static int output_fd[2];
+
+int main(int argc, char **argv) {
+    test_mode = 1;
+    
+    ui_init(NULL, argc, argv);
+    ui_onstartup(application_onstartup, NULL);
+    ui_onopen(application_onopen, NULL);
+    ui_onexit(application_onexit, NULL);
+    
+    int in = STDIN_FILENO;
+    int out = STDOUT_FILENO;
+    if(test_mode) {
+        if(pipe(input_fd)) {
+            perror("pipe");
+            return 1;
+        }
+        if(pipe(output_fd)) {
+            perror("pipe");
+            return 1;
+        }
+        
+        in = input_fd[0];
+        out = output_fd[1];
+        
+        pthread_t tid;
+        if(pthread_create(&tid, NULL, testwindow_read_thread, NULL)) {
+            perror("pthread_create");
+            return 1;
+        }
+        if(pthread_detach(tid)) {
+            perror("pthread_detach");
+            return 1;
+        }
+    }
+    MessageHandler *h = simple_msg_handler(in, out, client_msg_received);
+    client_init(h);
+    h->start(h);
+    
+    ui_main();
+    
+    h->stop(h);
+    fprintf(stderr, "client: end");
+    
+    return 0;
+}
+
+void application_onstartup(UiEvent *event, void *userdata) {
+    // We need at least one window for the event loop to work.
+    // Create a debug window, that is invisible by default
+    debug_window = ui_simple_window("debug", NULL);
+    // TODO: debug UI
+    
+    if(test_mode) {
+        testwindow_create();
+    }
+}
+
+void application_onopen(UiEvent *event, void *userdata) {
+    
+}
+
+void application_onexit(UiEvent *event, void *userdata) {
+    
+}
+
+static void testwindow_close(UiEvent *event, void *userdata) {
+    ui_close(debug_window);
+    ui_app_quit();
+    exit(0);
+}
+
+static void testwindow_send(UiEvent *event, void *userdata) {
+    TestWindow *window = event->window;
+    
+    char *str = ui_get(window->input);
+    int len = strlen(str);
+    cxmutstr msg = cx_asprintf("%d\n%s", len, str);
+    write(input_fd[1], msg.ptr, msg.length);
+    free(msg.ptr);
+    
+    ui_set(window->input, "");
+}
+
+static void testwindow_clear_output(UiEvent *event, void *userdata) {
+    TestWindow *window = event->window;
+    ui_set(window->output, "");
+}
+
+void testwindow_create(void) {
+    UiObject *obj = ui_simple_window("Test", NULL);
+    ui_context_closefunc(obj->ctx, testwindow_close, NULL);
+    ui_window_size(obj, 1800, 1400);
+    
+    TestWindow *window = ui_malloc(obj->ctx, sizeof(TestWindow));
+    window->input = ui_text_new(obj->ctx, NULL);
+    window->output = ui_text_new(obj->ctx, NULL);
+    obj->window = window;
+    
+    ui_hsplitpane(obj, .fill = TRUE, .initial_position = 900) {
+        // left
+        ui_vbox(obj, .fill = TRUE) {
+            ui_grid(obj, .margin = 10, .columnspacing = 10, .rowspacing = 10, .fill = TRUE) {
+                ui_llabel(obj, .label = "Input", .style = UI_LABEL_STYLE_TITLE, .hexpand = TRUE, .hfill = TRUE);
+                ui_newline(obj);
+                ui_textarea(obj, .value = window->input, .fill = TRUE);
+                ui_newline(obj);
+                ui_button(obj, .label = "Send", .onclick = testwindow_send);
+            }
+        }
+        
+        
+        // right
+        ui_vbox(obj, .fill = TRUE) {
+            ui_grid(obj, .margin = 10, .columnspacing = 10, .rowspacing = 10, .fill = TRUE) {
+                ui_llabel(obj, .label = "Output", .style = UI_LABEL_STYLE_TITLE, .hexpand = TRUE, .hfill = TRUE);
+                ui_newline(obj);
+                ui_textarea(obj, .value = window->output, .fill = TRUE);
+                ui_newline(obj);
+                ui_button(obj, .label = "Clear", .onclick = testwindow_clear_output);
+            }
+        }
+    }
+    
+    ui_show(obj);
+    
+    test_window = obj;
+}
+
+static int append_log(void *data) {
+    char *msg = data;
+    TestWindow *window = test_window->window;
+    UiText *out = window->output;
+    int length = out->length(out);
+    out->insert(out, length, msg);
+    free(msg);
+    return 0;
+}
+
+void* testwindow_read_thread(void *data) {
+    char buf[4096];
+    ssize_t r;
+    while((r = read(output_fd[0], buf, 4096)) > 0) {
+        char *str = malloc(r+1);
+        memcpy(str, buf, r);
+        str[r] = 0;
+        ui_call_mainthread(append_log, str);
+    }
+    
+    return NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/main.h	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,59 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CLIENT_MAIN_H
+#define CLIENT_MAIN_H
+
+#include <ui/ui.h>
+
+#include "message.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct TestWindow {
+    UiText *input;
+    UiText *output;
+} TestWindow;
+    
+void application_onstartup(UiEvent *event, void *userdata);
+void application_onopen(UiEvent *event, void *userdata);
+void application_onexit(UiEvent *event, void *userdata);
+
+void testwindow_create(void);
+
+void* testwindow_read_thread(void *data);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CLIENT_MAIN_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/message.c	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,174 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "message.h"
+
+MessageHandler* simple_msg_handler(int in, int out, msg_received_callback callback) {
+    SimpleMessageHandler *handler = malloc(sizeof(SimpleMessageHandler));
+    handler->handler.start = simple_msg_handler_start;
+    handler->handler.stop = simple_msg_handler_stop;
+    handler->handler.send = simple_msg_handler_send;
+    handler->handler.callback = callback;
+    handler->in = in;
+    handler->out = out;
+    handler->outbuf = cxBufferCreate(NULL, 4096, NULL, CX_BUFFER_FREE_CONTENTS | CX_BUFFER_AUTO_EXTEND);
+    handler->stop = 0;
+    pthread_mutex_init(&handler->queue_lock, NULL);
+    pthread_mutex_init(&handler->avlbl_lock, NULL);
+    pthread_cond_init(&handler->available, NULL);  
+    return (MessageHandler*)handler;
+}
+
+int simple_msg_handler_start(MessageHandler *handler) {
+    SimpleMessageHandler *sh = (SimpleMessageHandler*)handler;
+    if(pthread_create(&sh->in_thread, NULL, simple_msg_handler_in_thread, sh)) {
+        return 1;
+    }
+    if(pthread_create(&sh->out_thread, NULL, simple_msg_handler_out_thread, sh)) {
+        return 1;
+    }
+    return 0;
+}
+
+int simple_msg_handler_stop(MessageHandler *handler) {
+    SimpleMessageHandler *sh = (SimpleMessageHandler*)handler;
+    pthread_mutex_lock(&sh->queue_lock);
+    sh->stop = 0;
+    pthread_cond_signal(&sh->available);
+    pthread_mutex_unlock(&sh->queue_lock);
+    close(sh->in);
+    sh->in = -1;
+    
+    pthread_join(sh->in_thread, NULL);
+    pthread_join(sh->out_thread, NULL);
+    
+    return 0;
+}
+
+int simple_msg_handler_send(MessageHandler *handler, cxstring msg) {
+    SimpleMessageHandler *sh = (SimpleMessageHandler*)handler;
+    pthread_mutex_lock(&sh->queue_lock);
+    cxBufferWrite(msg.ptr, 1, msg.length, sh->outbuf);
+    pthread_cond_signal(&sh->available);
+    pthread_mutex_unlock(&sh->queue_lock);
+    return 0;
+}
+
+#define HEADERBUF_SIZE 64
+
+void* simple_msg_handler_in_thread(void *data) {
+    SimpleMessageHandler *handler = data;
+    
+    char *msg = NULL;
+    size_t msg_size = 0;
+    size_t msg_pos = 0; // currently received message length
+    
+    char headerbuf[HEADERBUF_SIZE];
+    size_t headerpos = 0;
+    
+    char buf[2048];
+    ssize_t r;
+    while((r = read(handler->in, buf, 2024)) > 0) {
+        char *buffer = buf;
+        size_t available = r;
+        
+        while(available > 0) {
+            if(msg) {
+                // read message
+                size_t need = msg_size - msg_pos;
+                size_t cplen = r > need ? need : available;
+                memcpy(msg+msg_pos, buffer, cplen);
+                buffer += cplen;
+                available -= cplen;
+                msg_pos += cplen;
+                if(msg_pos == msg_size) {
+                    // message complete
+                    //fprintf(stderr, "send: %.*s\n", (int)msg_size, msg);
+                    if(handler->handler.callback) {
+                        handler->handler.callback(cx_mutstrn(msg, msg_size));
+                    }
+                    msg = NULL;
+                    msg_size = 0;
+                    msg_pos = 0;
+                }
+            } else {
+                size_t header_max = HEADERBUF_SIZE - headerpos - 1;
+                if(header_max > available) {
+                    header_max = available;
+                }
+                // search for line break
+                int i;
+                int header_complete = 0;
+                for(i=0;i<header_max;i++) {
+                    if(buffer[i] == '\n') {
+                        header_complete = 1;
+                        break;
+                    }
+                }
+                i++;
+                memcpy(headerbuf+headerpos, buffer, i);
+                headerpos += i;
+                buffer += i;
+                available -= i;
+                
+                if(header_complete) {
+                    headerbuf[headerpos-1] = 0; // terminate buffer
+                    char *end;
+                    long length = strtol(headerbuf, &end, 10);
+                    if(*end == '\0') {
+                        //fprintf(stderr, "header: %d\n", (int)length);
+                        msg = malloc(length);
+                        msg_size = length;
+                        headerpos = 0;
+                    } else {
+                        fprintf(stderr, "Error: invalid message {%s}\n", headerbuf);
+                    }
+                } else if(headerpos+1 >= HEADERBUF_SIZE) {
+                    fprintf(stderr, "Error: message header too big\n");
+                    exit(-1);
+                }
+            }
+        }
+        
+        
+    }
+    perror("error");
+    fprintf(stderr, "stop simple_msg_handler_in_thread\n");
+    
+    return NULL;
+}
+
+void* simple_msg_handler_out_thread(void *data) {
+    SimpleMessageHandler *handler = data;
+    
+    return NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/message.h	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,81 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CLIENT_MESSAGE_H
+#define CLIENT_MESSAGE_H
+
+#include <cx/string.h>
+#include <cx/json.h>
+#include <cx/buffer.h>
+
+#include <pthread.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct MessageHandler MessageHandler;
+
+typedef void(*msg_received_callback)(cxmutstr msg);
+
+struct MessageHandler {
+    int (*start)(MessageHandler *handler);
+    int (*stop)(MessageHandler *handler);
+    int (*send)(MessageHandler *handler, cxstring msg);
+    
+    msg_received_callback callback;
+};
+
+typedef struct SimpleMessageHandler {
+    MessageHandler handler;
+    int in;
+    int out;
+    pthread_t in_thread;
+    pthread_t out_thread;
+    pthread_mutex_t queue_lock;
+    pthread_mutex_t avlbl_lock;
+    pthread_cond_t  available;
+    CxBuffer *outbuf;
+    int stop;
+} SimpleMessageHandler;
+
+MessageHandler* simple_msg_handler(int in, int out, msg_received_callback callback);
+int simple_msg_handler_start(MessageHandler *handler);
+int simple_msg_handler_stop(MessageHandler *handler);
+int simple_msg_handler_send(MessageHandler *handler, cxstring msg);
+
+void* simple_msg_handler_in_thread(void *data);
+void* simple_msg_handler_out_thread(void *data);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CLIENT_MESSAGE_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/uiclient.c	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,254 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "uiclient.h"
+#include "args.h"
+
+#include <cx/hash_map.h>
+
+#include "../ui/common/args.h"
+
+static MessageHandler *io;
+
+static CxMap *msg_types;
+static CxMap *objects;
+
+void client_init(MessageHandler *handler) {
+    io = handler;
+    
+    msg_types = cxHashMapCreateSimple(CX_STORE_POINTERS);
+    cxMapPut(msg_types, "simple_window", msg_simple_window);
+    cxMapPut(msg_types, "show", msg_show);
+    
+    cxMapPut(msg_types, "vbox", msg_vbox);
+    cxMapPut(msg_types, "hbox", msg_vbox);
+    cxMapPut(msg_types, "grid", msg_vbox);
+    cxMapPut(msg_types, "end", msg_end);
+    cxMapPut(msg_types, "button", msg_button);
+    
+    objects = cxHashMapCreateSimple(CX_STORE_POINTERS);
+}
+
+static cxmutstr jsonobj_getstring(const CxJsonValue *obj, const char *name) {
+    CxJsonValue *value = cxJsonObjGet(obj, name);
+    if(value->type == CX_JSON_STRING) {
+        return value->value.string;
+    } else {
+        return (cxmutstr){ NULL, 0 };
+    }
+}
+
+/*
+ * UI thread callback
+ */
+static int msg_received(void *data) {
+    CxJsonValue *value = data;
+    if(client_handle_json(NULL, value)) {
+        fprintf(stderr, "Error: invalid json message\n");
+    }
+    cxJsonValueFree(value);
+    return 0;
+}
+
+/*
+ * This message callback is executed in the message handler input thread
+ */
+void client_msg_received(cxmutstr msg) {
+    // parse message
+    CxJson json;
+    cxJsonInit(&json, NULL);
+    
+    cxJsonFilln(&json, msg.ptr, msg.length);
+    CxJsonValue *value;
+    if(cxJsonNext(&json, &value) == CX_JSON_NO_ERROR && value) {
+        // handle json message in the UI thread
+        ui_call_mainthread(msg_received, value);
+    } else {
+        fprintf(stderr, "Error: invalid json message\n");
+    }
+    cxJsonDestroy(&json);
+}
+
+int client_handle_json(UiObject *obj, const CxJsonValue *value) {
+    if(value->type != CX_JSON_OBJECT) {
+        return 1;
+    }
+    
+    CxJsonValue *type = cxJsonObjGet(value, "type");
+    if(!type || type->type != CX_JSON_STRING) {
+        return 1;
+    }
+    
+    json_msg_handler handler = cxMapGet(msg_types, type->value.string);
+    if(!handler) {
+        return 1;
+    }
+    
+    return handler(obj, value);
+}
+
+int client_handle_children(UiObject *parent, const CxJsonValue *value) {
+    CxJsonValue *children = cxJsonObjGet(value, "children");
+    if(children && children->type == CX_JSON_ARRAY) {
+        for(int i=0;i<children->value.array.array_size;i++) {
+            CxJsonValue *child = children->value.array.array[i];
+            if(client_handle_json(parent, child)) {
+                fprintf(stderr, "Error: invalid child\n");
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+void client_add_obj_mapping(UiObject *obj, cxmutstr id) {
+    cxMapPut(objects, id, obj);
+    
+    CxAllocator *a = ui_allocator(obj->ctx);
+    
+    WindowData *wdata = cxMalloc(a, sizeof(WindowData));
+    wdata->widgets = cxHashMapCreate(a, CX_STORE_POINTERS, 128);
+    obj->window = wdata;
+}
+
+UiObject* client_get_mapped_obj(cxmutstr id) {
+    return cxMapGet(objects, id);
+}
+
+void client_reg_widget(UiObject *obj, cxmutstr id, UIWIDGET w) {
+    WindowData *wdata = obj->window;
+    if(!wdata) {
+        fprintf(stderr, "Error: missing obj window data\n");
+        return;
+    }
+    cxMapPut(wdata->widgets, id, w);
+}
+
+UIWIDGET client_get_widget(UiObject *obj, cxmutstr id) {
+    WindowData *wdata = obj->window;
+    if(!wdata) {
+        fprintf(stderr, "Error: missing obj window data\n");
+        return NULL;
+    }
+    return cxMapGet(wdata->widgets, id);
+}
+
+static UiObject* get_msg_obj(UiObject *obj, const CxJsonValue *value) {
+    if(obj) {
+        return obj;
+    }
+    CxJsonValue *obj_id = cxJsonObjGet(value, "obj");
+    if(!obj_id || obj_id->type != CX_JSON_STRING) {
+        return NULL;
+    }
+    return client_get_mapped_obj(obj_id->value.string);
+}
+
+int msg_simple_window(UiObject *parent, const CxJsonValue *value) {
+    cxmutstr id = jsonobj_getstring(value, "id");
+    cxmutstr title = jsonobj_getstring(value, "title");
+    
+    if(!id.ptr) {
+        return 1;
+    }
+    
+    UiObject *obj = ui_simple_window(title.ptr, NULL);
+    client_add_obj_mapping(obj, id);
+    
+    return client_handle_children(obj, value);
+}
+
+int msg_show(UiObject *parent, const CxJsonValue *value) {
+    UiObject *obj = client_get_mapped_obj(jsonobj_getstring(value, "obj"));
+    if(!obj) {
+        return 1;
+    }
+    ui_show(obj);
+    return 0;
+}
+
+typedef UIWIDGET(*ctcreate_func)(UiObject *obj, UiContainerArgs *args);
+
+static int msg_container(UiObject *parent, const CxJsonValue *value, ctcreate_func create) {
+    CxJsonValue *args_value = cxJsonObjGet(value, "args");
+    cxmutstr id = jsonobj_getstring(value, "id");
+    if(!id.ptr) {
+        return 1;
+    }
+    UiObject *obj = get_msg_obj(parent, value);
+    if(!obj) {
+        return 1;
+    }
+    
+    UiContainerArgs *args = json2container_args(args_value);
+    UIWIDGET w = create(obj, args);
+    ui_container_args_free(args);
+    client_reg_widget(obj, id, w);
+    
+    return 0;
+}
+
+int msg_vbox(UiObject *parent, const CxJsonValue *value) {
+    return msg_container(parent, value, ui_vbox_create);
+}
+
+int msg_hbox(UiObject *parent, const CxJsonValue *value) {
+    return msg_container(parent, value, ui_hbox_create);
+}
+
+int msg_grid(UiObject *parent, const CxJsonValue *value) {
+    return msg_container(parent, value, ui_grid_create);
+}
+
+int msg_end(UiObject *parent, const CxJsonValue *value) {
+    UiObject *obj = get_msg_obj(parent, value);
+    if(!obj) {
+        return 1;
+    }
+    ui_end_new(obj);
+    return 0;
+}
+
+int msg_button(UiObject *parent, const CxJsonValue *value) {
+    CxJsonValue *args_value = cxJsonObjGet(value, "args");
+    cxmutstr id = jsonobj_getstring(value, "id");
+    if(!id.ptr) {
+        return 1;
+    }
+    UiObject *obj = get_msg_obj(parent, value);
+    if(!obj) {
+        return 1;
+    }
+    
+    UiButtonArgs *args = json2button_args(args_value);
+    UIWIDGET w = ui_button_create(obj, args);
+    ui_button_args_free(args);
+    client_reg_widget(obj, id, w);
+    
+    return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/uiclient.h	Sun Nov 30 18:15:46 2025 +0100
@@ -0,0 +1,82 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include <ui/ui.h>
+#include <cx/string.h>
+#include <cx/json.h>
+#include <cx/map.h>
+
+#include "message.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+    
+typedef struct WindowData {
+    CxMap *widgets;
+} WindowData;
+    
+typedef int (*json_msg_handler)(UiObject *parent, const CxJsonValue *value);
+    
+void client_init(MessageHandler *handler);
+    
+void client_msg_received(cxmutstr msg);
+
+int client_handle_json(UiObject *obj, const CxJsonValue *value);
+
+int client_handle_children(UiObject *parent, const CxJsonValue *value);
+
+void client_add_obj_mapping(UiObject *obj, cxmutstr id);
+UiObject* client_get_mapped_obj(cxmutstr id);
+void client_reg_widget(UiObject *obj, cxmutstr id, UIWIDGET w);
+UIWIDGET client_get_widget(UiObject *obj, cxmutstr id);
+
+
+int msg_simple_window(UiObject *parent, const CxJsonValue *value);
+int msg_show(UiObject *parent, const CxJsonValue *value);
+
+int msg_vbox(UiObject *parent, const CxJsonValue *value);
+int msg_hbox(UiObject *parent, const CxJsonValue *value);
+int msg_grid(UiObject *parent, const CxJsonValue *value);
+
+int msg_end(UiObject *parent, const CxJsonValue *value);
+
+int msg_button(UiObject *parent, const CxJsonValue *value);
+
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CLIENT_H */
+
--- a/configure	Sun Nov 30 14:40:47 2025 +0100
+++ b/configure	Sun Nov 30 18:15:46 2025 +0100
@@ -114,6 +114,9 @@
 Options:
   --toolkit=(libadwaita|gtk4|gtk3|gtk2|gtk2legacy|qt5|qt4|cocoa|motif)
 
+Optional Features:
+  --enable-client
+
 __EOF__
 }
 
@@ -180,6 +183,8 @@
         "--release")          BUILD_TYPE="release" ;;
         "--toolkit="*) OPT_TOOLKIT=${ARG#--toolkit=} ;;
         "--toolkit")  echo "option '$ARG' needs a value:"; echo "  $ARG=(libadwaita|gtk4|gtk3|gtk2|gtk2legacy|qt5|qt4|cocoa|motif)"; abort_configure ;;
+        "--enable-client") FEATURE_CLIENT=on ;;
+        "--disable-client") unset FEATURE_CLIENT ;;
         "-"*) echo "unknown option: $ARG"; abort_configure ;;
     esac
 done
@@ -1103,6 +1108,40 @@
     echo "TK_LDFLAGS += $TEMP_LDFLAGS" >> "$TEMP_DIR/flags.mk"
 fi
 
+echo >> "$TEMP_DIR/flags.mk"
+echo "configuring target: client"
+echo "# flags for target client" >> "$TEMP_DIR/flags.mk"
+TEMP_CFLAGS=
+TEMP_CXXFLAGS=
+TEMP_LDFLAGS=
+
+
+# Features
+if [ -n "$FEATURE_CLIENT" ]; then
+    if [ -n "$DISABLE_FEATURE_CLIENT" ]; then
+        unset FEATURE_CLIENT
+    fi
+fi
+if [ -n "$FEATURE_CLIENT" ]; then
+    :
+else
+    :
+    cat >> "$TEMP_DIR/make.mk" << __EOF__
+DISABLE_CLIENT=1
+__EOF__
+fi
+
+
+if [ -n "${TEMP_CFLAGS}" ] && [ -n "$lang_c" ]; then
+    echo "CLIENT_CFLAGS  += $TEMP_CFLAGS" >> "$TEMP_DIR/flags.mk"
+fi
+if [ -n "${TEMP_CXXFLAGS}" ] && [ -n "$lang_cpp" ]; then
+    echo "CLIENT_CXXFLAGS  += $TEMP_CXXFLAGS" >> "$TEMP_DIR/flags.mk"
+fi
+if [ -n "${TEMP_LDFLAGS}" ]; then
+    echo "CLIENT_LDFLAGS += $TEMP_LDFLAGS" >> "$TEMP_DIR/flags.mk"
+fi
+
 
 # final result
 if [ $ERROR -ne 0 ]; then
@@ -1178,6 +1217,13 @@
 echo "Options:"
 cat "$TEMP_DIR/options"
 echo
+echo "Features:"
+if [ -n "$FEATURE_CLIENT" ]; then
+echo "  client: on"
+else
+echo "  client: off"
+fi
+echo
 
 # generate the config.mk file
 pwd=`pwd`
--- a/make/Makefile.mk	Sun Nov 30 14:40:47 2025 +0100
+++ b/make/Makefile.mk	Sun Nov 30 18:15:46 2025 +0100
@@ -32,10 +32,10 @@
 include config.mk
 
 BUILD_DIRS = build/bin build/lib
-BUILD_DIRS += build/application build/ucx
+BUILD_DIRS += build/application build/client build/ucx 
 BUILD_DIRS += build/ui/common build/ui/$(TOOLKIT)
 
-all: $(BUILD_DIRS) ucx ui application
+all: $(BUILD_DIRS) ucx ui application client
 	make/$(PACKAGE_SCRIPT)
 
 $(BUILD_DIRS):
@@ -50,5 +50,8 @@
 application: ui FORCE
 	cd application; $(MAKE)
 
+client: ui FORCE
+	cd client; $(MAKE)
+
 FORCE:
 
--- a/make/project.xml	Sun Nov 30 14:40:47 2025 +0100
+++ b/make/project.xml	Sun Nov 30 18:15:46 2025 +0100
@@ -175,5 +175,13 @@
 			<default value="motif" />
 		</option>
 	</target>
+	
+	<target name="client">
+		<feature name="client" default="false">
+			<disabled>
+                <make>DISABLE_CLIENT=1</make>
+            </disabled>
+		</feature>
+	</target>
 </project>
 
--- a/make/toolchain.sh	Sun Nov 30 14:40:47 2025 +0100
+++ b/make/toolchain.sh	Sun Nov 30 18:15:46 2025 +0100
@@ -3,12 +3,19 @@
 # toolchain detection
 #
 
+TAIL="tail"
 if isplatform "bsd" && notisplatform "openbsd"; then
   C_COMPILERS="clang gcc cc"
   CPP_COMPILERS="clang++ g++ CC"
+elif isplatform "solaris"; then
+  C_COMPILERS="cc suncc gcc clang"
+  CPP_COMPILERS="CC sunCC g++ clang++"
+  if [ -f /usr/xpg4/bin/tail ]; then
+    TAIL=/usr/xpg4/bin/tail
+  fi
 else
-  C_COMPILERS="gcc clang suncc cc"
-  CPP_COMPILERS="g++ clang++ sunCC CC"
+  C_COMPILERS="gcc clang cc"
+  CPP_COMPILERS="g++ clang++ c++"
 fi
 unset TOOLCHAIN
 unset TOOLCHAIN_NAME
@@ -17,14 +24,14 @@
 
 check_c_compiler()
 {
-  command -v $1 2>&1 >/dev/null
-  if [ $? -ne 0 ]; then
+  command -v "$1" >/dev/null 2>&1
+  if [ $? -ne 0 ] ; then
     return 1
   fi
   cat > "$TEMP_DIR/test.c" << __EOF__
 /* test file */
 #include <stdio.h>
-int main(int argc, char **argv) {
+int main(void) {
 #if defined(_MSC_VER)
   printf("toolchain:msc\n");
 #elif defined(__clang__)
@@ -38,7 +45,7 @@
 #endif
   printf("wsize:%d\n", (int)sizeof(void*)*8);
 #ifdef __STDC_VERSION__
-  printf("stdcversion:%d\n", __STDC_VERSION__);
+  printf("stdcversion:%ld\n", (long int)__STDC_VERSION__);
 #endif
   return 0;
 }
@@ -49,14 +56,14 @@
 
 check_cpp_compiler()
 {
-  command -v $1 2>&1 >/dev/null
-  if [ $? -ne 0 ]; then
+  command -v "$1" >/dev/null 2>&1
+  if [ $? -ne 0 ] ; then
     return 1
   fi
   cat > "$TEMP_DIR/test.cpp" << __EOF__
 /* test file */
 #include <iostream>
-int main(int argc, char **argv) {
+int main(void) {
 #if defined(_MSC_VER)
   std::cout << "toolchain:msc" << std::endl;
 #elif defined(__clang__)
@@ -76,62 +83,12 @@
   $1 -o "$TEMP_DIR/checkcc" $CXXFLAGS $LDFLAGS "$TEMP_DIR/test.cpp" 2> /dev/null
 }
 
-create_libtest_source()
-{
-  # $1: filename
-  # $2: optional include
-  cat > "$TEMP_DIR/$1" << __EOF__
-/* libtest file */
-int main(int argc, char **argv) {
-  return 0;
-}
-__EOF__
-  if [ -n "$2" ]; then
-    echo "#include <$2>" >> "$TEMP_DIR/$1"
-  fi
-}
-
-check_c_lib()
-{
-  # $1: libname
-  # $2: optional include
-  if [ -z "$TOOLCHAIN_CC" ]; then
-    return 1
-  fi
-  create_libtest_source "test.c" "$2"
-  rm -f "$TEMP_DIR/checklib"
-  $TOOLCHAIN_CC -o "$TEMP_DIR/checklib" $CFLAGS $LDFLAGS "-l$1" "$TEMP_DIR/test.c" 2> /dev/null
-}
-
-check_cpp_lib()
-{
-  # $1: libname
-  # $2: optional include
-  if [ -z "$TOOLCHAIN_CXX" ]; then
-    return 1
-  fi
-  create_libtest_source "test.cpp" "$2"
-  rm -f "$TEMP_DIR/checklib"
-  $TOOLCHAIN_CXX -o "$TEMP_DIR/checklib" $CXXFLAGS $LDFLAGS "-l$1" "$TEMP_DIR/test.cpp" 2> /dev/null
-}
-
-check_lib()
-{
-  # $1: libname
-  # $2: optional include
-  if [ -n "$TOOLCHAIN_CC" ]; then
-    check_c_lib "$1" "$2"
-  elif  [ -n "$TOOLCHAIN_CXX" ]; then
-    check_cpp_lib "$1" "$2"
-  fi
-}
-
 parse_toolchain_properties()
 {
   info_file="$1"
-  TOOLCHAIN=`grep '^toolchain:' "$info_file" | tail -c +11`
+  TOOLCHAIN=`grep '^toolchain:' "$info_file" | $TAIL -c +11`
   TOOLCHAIN_NAME=`echo "$TOOLCHAIN" | cut -f1 -d' ' -`
-  TOOLCHAIN_WSIZE=`grep '^wsize:' "$info_file" | tail -c +7`
+  TOOLCHAIN_WSIZE=`grep '^wsize:' "$info_file" | $TAIL -c +7`
 }
 
 detect_c_compiler()
@@ -145,7 +102,7 @@
       TOOLCHAIN_CC=$CC
       "$TEMP_DIR/checkcc" > "$TEMP_DIR/checkcc_out"
       parse_toolchain_properties "$TEMP_DIR/checkcc_out"
-      TOOLCHAIN_CSTD=`grep '^stdcversion:' "$TEMP_DIR/checkcc_out" | tail -c +13`
+      TOOLCHAIN_CSTD=`grep '^stdcversion:' "$TEMP_DIR/checkcc_out" | $TAIL -c +13`
       echo "$CC"
       return 0
     else
@@ -159,7 +116,7 @@
         TOOLCHAIN_CC=$COMP
         "$TEMP_DIR/checkcc" > "$TEMP_DIR/checkcc_out"
         parse_toolchain_properties "$TEMP_DIR/checkcc_out"
-        TOOLCHAIN_CSTD=`grep '^stdcversion:' "$TEMP_DIR/checkcc_out" | tail -c +13`
+        TOOLCHAIN_CSTD=`grep '^stdcversion:' "$TEMP_DIR/checkcc_out" | $TAIL -c +13`
         echo "$COMP"
         return 0
       fi
--- a/ui/common/args.h	Sun Nov 30 14:40:47 2025 +0100
+++ b/ui/common/args.h	Sun Nov 30 18:15:46 2025 +0100
@@ -137,7 +137,7 @@
 UIEXPORT void ui_container_args_set_margin_left(UiContainerArgs *args, int value);
 UIEXPORT void ui_container_args_set_margin_right(UiContainerArgs *args, int value);
 UIEXPORT void ui_container_args_set_margin_top(UiContainerArgs *args, int value);
-UIEXPORT void ui_container_args_set_margin_right(UiContainerArgs *args, int value);
+UIEXPORT void ui_container_args_set_margin_bottom(UiContainerArgs *args, int value);
 UIEXPORT void ui_container_args_set_colspan(UiContainerArgs *args, int colspan);
 UIEXPORT void ui_container_args_set_rowspan(UiContainerArgs *args, int rowspan);
 UIEXPORT void ui_container_args_set_def_hexpand(UiContainerArgs *args, UiBool value);
--- a/ui/gtk/toolkit.c	Sun Nov 30 14:40:47 2025 +0100
+++ b/ui/gtk/toolkit.c	Sun Nov 30 18:15:46 2025 +0100
@@ -164,7 +164,7 @@
 
 #ifndef UI_GTK2
 void ui_app_quit() {
-    g_application_quit(G_APPLICATION(app));
+    g_application_quit(G_APPLICATION(app)); // TODO: fix, does not work
 }
 
 GtkApplication* ui_get_application() {

mercurial