From b41ec911812e66931f01939378979845716b6119 Mon Sep 17 00:00:00 2001
From: dweller <dweller@cabin.digital>
Date: Fri, 28 Mar 2025 19:52:47 +0200
Subject: experimenting with UI

---
 Makefile           |  24 ++++
 build/x11          |  23 ----
 clean              |   2 -
 resources/icon.tga | Bin 0 -> 1068 bytes
 resources/icon.xcf | Bin 0 -> 2778 bytes
 sources/bits.c     |  72 +++++++++--
 sources/main.c     | 343 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 sources/tga.c      | 138 +++++++++++++++++++++
 sources/x11.c      | 294 +++++++++++++++++++++++++++++++++++++++++++++
 utils/bin2.c       |  79 ++++++++++++
 10 files changed, 937 insertions(+), 38 deletions(-)
 create mode 100644 Makefile
 delete mode 100755 build/x11
 delete mode 100755 clean
 create mode 100644 resources/icon.tga
 create mode 100644 resources/icon.xcf
 create mode 100644 sources/tga.c
 create mode 100644 sources/x11.c
 create mode 100755 utils/bin2.c

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4f32fb4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+.POSIX:
+.SUFFIXES:
+
+CC       = gcc
+CFLAGS   = -std=c89 -Wall -Wextra -fwhole-program
+X11FLAGS = -lm -lX11 -lxcb -lXau -lXdmcp
+
+x11: artifacts/xip-8
+
+artifacts/xip-8: sources/*.c resources/icon.tga.h
+	$(CC) $(CFLAGS) $(X11FLAGS) sources/main.c -o artifacts/xip-8
+
+artifacts/bin2c: utils/bin2.c
+	$(CC) $(CFLAGS) utils/bin2.c -o artifacts/bin2c
+
+resources/icon.tga.h: resources/icon.tga artifacts/bin2c
+	./artifacts/bin2c resources/icon.tga
+
+run: x11
+	./artifacts/xip-8
+
+clean:
+	rm -f resources/*.h
+	rm -rf artifacts/*
diff --git a/build/x11 b/build/x11
deleted file mode 100755
index cae1518..0000000
--- a/build/x11
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/sh -e
-
-CC=${CC:-musl-gcc}
-STATIC=${STATIC:-"-static"}
-CFLAGS=" \
-    -std=c89 \
-    -Wall -Wextra -Wno-comment \
-    -ggdb -Og \
-"
-
-bench()
-{
-    if [ x = "x$(which time 2>/dev/null)" ]; then
-        $@
-        echo "time not installed"
-    else
-        time $@
-    fi
-}
-
-mkdir -p artifacts
->&2 bench $CC $CFLAGS sources/main.c $STATIC -o artifacts/xip-8
->&2 echo "------------------------------------------------\n"
diff --git a/clean b/clean
deleted file mode 100755
index 1dd2ea1..0000000
--- a/clean
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh -e
-rm -rf artifacts/*
diff --git a/resources/icon.tga b/resources/icon.tga
new file mode 100644
index 0000000..73d0cd3
Binary files /dev/null and b/resources/icon.tga differ
diff --git a/resources/icon.xcf b/resources/icon.xcf
new file mode 100644
index 0000000..25cb8c1
Binary files /dev/null and b/resources/icon.xcf differ
diff --git a/sources/bits.c b/sources/bits.c
index 0197436..de0aa0d 100644
--- a/sources/bits.c
+++ b/sources/bits.c
@@ -5,22 +5,70 @@
  * See LICENSE for details
  */
 
+typedef unsigned char       u8;
+typedef unsigned short      u16;
+typedef unsigned int        u32;
+typedef unsigned long long  u64; /* XXX: GCC extension */
+typedef   signed char       s8;
+typedef   signed short      s16;
+typedef   signed int        s32;
+typedef   signed long long  s64; /* XXX: GCC extension */
+
+typedef s8 bool;
+
+typedef u64 usize;
+typedef s64 ssize;
+
+typedef float  f32;
+typedef double f64;
+
+
+#define min_s8    ((s8) (0x80))
+#define min_s16   ((s16)(0x8000))
+#define min_s32   ((s32)(0x80000000L))
+#define min_s64   ((s64)(0x8000000000000000LL))
+#define min_ssize min_s64
+
+#define max_s8    ((s8) (0x7F))
+#define max_s16   ((s16)(0x7FFF))
+#define max_s32   ((s32)(0x7FFFFFFFL))
+#define max_s64   ((s64)(0x7FFFFFFFFFFFFFFFLL))
+#define max_ssize max_s64
+
+#define max_u8    ((u8) (0xFF))
+#define max_u16   ((u16)(0xFFFF))
+#define max_u32   ((u32)(0xFFFFFFFFUL))
+#define max_u64   ((u64)(0xFFFFFFFFFFFFFFFFULL))
+#define max_usize max_u64
+
+#ifndef true
+    #define true  1
+#endif
+
+#ifndef false
+    #define false 0
+#endif
 
 #define iota __COUNTER__
+#define nil {0}
 
-#define lengthof(x) (sizeof(x) / sizeof((x)[0]))
+#define KB bit(10LL)
+#define MB bit(20LL)
+#define GB bit(30LL)
+#define TB bit(40LL)
 
-#define bit(x) (1 << (x))
+#define packed __attribute__((packed))
 
-#define KB bit(10)
-#define MB bit(11)
-#define GB bit(12)
+#define bit(n) (1LL << (n))
+#define align(what, to) (((what) + ((to) - 1)) & ~((to) - 1)) /* NOTE: `to` *must* be pow. of 2 */
 
-typedef unsigned char  u8;
-typedef unsigned short u16;
-typedef unsigned long long u64;
-typedef u64 usize;
+#define lengthof(arr) (sizeof(arr) / sizeof(arr[0]))
+#ifndef offsetof
+    #define offsetof(strct, memb) ((ssize)(&((strct*)(0)->memb)))
+#endif
+
+#define min(a, b)        (((a) <= (b)) ? (a) : (b))
+#define max(a, b)        (((a) >= (b)) ? (a) : (b))
+#define clamp(x, mn, mx) (min((mn), max((x), (mx))))
 
-typedef u8 bool;
-#define true  1
-#define false 0
+#define unused(expr) ((void)(expr))
diff --git a/sources/main.c b/sources/main.c
index 5bc1c42..791cf43 100644
--- a/sources/main.c
+++ b/sources/main.c
@@ -5,7 +5,6 @@
  * See LICENSE for details
  */
 
-
 #define _DEFAULT_SOURCE
 #include <stdlib.h>
 #include <stdio.h>
@@ -13,18 +12,359 @@
 #include <assert.h>
 #include <time.h>
 #include <stdarg.h>
+#include <ctype.h>
 
 #include <endian.h>
 
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <X11/Xutil.h>
+#include <X11/Xresource.h>
+
 #include "bits.c"
 #include "log.c"
+#include "tga.c"
+#include "x11.c"
 
 #include "chip8.c"
 #include "meta/disasm.c"
 #include "meta/exec.c"
 
 
+typedef struct
+{
+    void* usr;
+
+    usize hot;
+    usize active;
+    usize kbdfocus;
+
+    /* mouse */
+    u16 x;
+    u16 y;
+
+    u8 pbtns;
+    u8 btns;
+
+} iui;
+
+void iui_start(iui* ui, void* usr)
+{
+    assert(ui);
+
+    ui->usr = usr;
+    ui->hot = 0;
+}
+
+void iui_end(iui* ui)
+{
+    assert(ui);
+
+    if(!ui->btns) ui->active = 0;
+    else
+    {
+        if(!ui->active) ui->kbdfocus = 0;
+    }
+
+    ui->pbtns = ui->btns;
+}
+
+bool iui_mouse_inside(iui* ui, u16 x, u16 y, u16 w, u16 h)
+{
+    return !(ui->x < x || ui->y < y || ui->x >= x + w || ui->y >= y + h);
+}
+
+int iui_button(iui* ui, u16 x, u16 y, int w, int h, char* label)
+{
+    xctx*  x11 = (xctx*)ui->usr;
+    usize   id = (usize)label;
+    bool imhot, imactive;
+
+    if(iui_mouse_inside(ui, x, y, w, h))
+    {
+        bool click = ui->btns & bit(1) && !(ui->pbtns & bit(1));
+        if(!ui->active && click) ui->active = id;
+        ui->hot = id;
+    }
+
+    imhot    = ui->hot    == id;
+    imactive = ui->active == id;
+
+    {
+        XGCValues gcvals = {0};
+
+        if(imhot) gcvals.line_width = 2;
+        else gcvals.line_width = 1;
+
+        if(imactive) gcvals.foreground = x11->cols.as.fg;
+        else gcvals.foreground = x11->cols.as.bg;
+
+        XChangeGC(x11->disp, x11->gc, GCForeground | GCLineWidth, &gcvals);
+        XFillRectangle(x11->disp, x11->wnd, x11->gc, x, y, w, h);
+
+        XSetForeground(x11->disp, x11->gc, x11->cols.as.fg);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x, y, x, y + h);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x + w, y, x, y);
+
+        XSetForeground(x11->disp, x11->gc, x11->cols.as.fg);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x, y + h, x + w, y + h);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x + w, y + h, x + w, y);
+    }
+
+    {
+        usize len;
+        int xx, yy;
+        int di, fa, fd;
+        int width, height;
+        XCharStruct chs = {0};
+
+        len = strlen(label);
+        XTextExtents(x11->xfs, label, len, &di, &fa, &fd, &chs);
+        width  = chs.rbearing - chs.lbearing;
+        height = chs.ascent   + chs.descent;
+
+        xx = x + w / 2 - width / 2;
+        yy = y + h / 2 + height / 2;
+
+        if(imactive) XSetForeground(x11->disp, x11->gc, x11->cols.as.bg);
+        else XSetForeground(x11->disp, x11->gc, x11->cols.as.fg);
+        XDrawString(x11->disp, x11->wnd, x11->gc, xx, yy, label, len);
+    }
+
+    return imhot && imactive && !(ui->btns & bit(1));
+}
+
+int iui_textbox(iui* ui, u16 x, u16 y, int w, int h, char* buf, usize buflen)
+{
+    xctx*  x11 = (xctx*)ui->usr;
+    usize   id = (usize)buf;
+    bool imhot, imactive;
+    usize len;
+
+    len = strlen(buf);
+
+    if(iui_mouse_inside(ui, x, y, w, h))
+    {
+        bool click = ui->btns & bit(1) && !(ui->pbtns & bit(1));
+        if(!ui->active && click) ui->active = id;
+        ui->hot = id;
+    }
+
+    imhot    = ui->hot    == id;
+    imactive = ui->active == id;
+
+    {
+        XGCValues gcvals = {0};
+
+        if(imhot) gcvals.line_width = 2;
+        else gcvals.line_width = 1;
+
+        gcvals.foreground = x11->cols.as.bg;
+
+        XChangeGC(x11->disp, x11->gc, GCForeground | GCLineWidth, &gcvals);
+        XFillRectangle(x11->disp, x11->wnd, x11->gc, x, y, w, h);
+
+        XSetForeground(x11->disp, x11->gc, x11->cols.as.fg);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x, y, x, y + h);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x + w, y, x, y);
+
+        XSetForeground(x11->disp, x11->gc, x11->cols.as.fg);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x, y + h, x + w, y + h);
+        XDrawLine(x11->disp, x11->wnd, x11->gc, x + w, y + h, x + w, y);
+    }
+
+    if(imactive) ui->kbdfocus = id;
+    if(ui->kbdfocus == id)
+    {
+
+        XGCValues gcvals = {0};
+        gcvals.foreground = x11->cols.as.fg;
+
+        XChangeGC(x11->disp, x11->gc, GCForeground, &gcvals);
+        XFillRectangle(x11->disp, x11->wnd, x11->gc, x + 6 + (6 * len), y + 6, 10, h - 12);
+    }
+    else
+    {
+        XGCValues gcvals = {0};
+        gcvals.foreground = x11->cols.as.fg;
+        gcvals.line_width = 1;
+
+        XChangeGC(x11->disp, x11->gc, GCForeground | GCLineWidth, &gcvals);
+        XDrawRectangle(x11->disp, x11->wnd, x11->gc, x + 6 + (6 * len), y + 6, 9, h - 13);
+    }
+
+    {
+        int xx, yy;
+        int di, fa, fd;
+        int width, height;
+        XCharStruct chs = {0};
+
+        XTextExtents(x11->xfs, buf, buflen, &di, &fa, &fd, &chs);
+        height = chs.ascent   + chs.descent;
+
+        xx = x + 6;
+        yy = y + h / 2 + height / 2;
+
+
+        if(imactive) XSetForeground(x11->disp, x11->gc, x11->cols.as.bg);
+        else XSetForeground(x11->disp, x11->gc, x11->cols.as.fg);
+        XDrawString(x11->disp, x11->wnd, x11->gc, xx, yy, buf, len);
+    }
+
+    return imhot && imactive && !(ui->btns & bit(1));
+}
+
+
 int main(int argc, char** argv)
+{
+    xctx x11 = {0};
+    iui   ui = {0};
+    bool run = true;
+    char txtbuf[256] = {0};
+
+    x11_init(800, 600, "xip-8", argc, argv, &x11);
+
+    while(run)
+    {
+        XEvent ev;
+
+        while(XCheckTypedWindowEvent(x11.disp, x11.wnd, ClientMessage, &ev))
+            if((Atom)ev.xclient.data.l[0] == x11.xa_delwnd) run = false;
+
+        while(XCheckWindowEvent(x11.disp, x11.wnd, max_u32, &ev))
+        {
+            bool keyup = false;
+            switch(ev.type)
+            {
+                case KeymapNotify:
+                    XRefreshKeyboardMapping(&ev.xmapping);
+                    break;
+
+                case KeyRelease:
+                    keyup = true;
+                    /* fall through */
+                case KeyPress:
+                {
+                    KeySym ks;
+                    char name[32] = {0};
+                    int len = 0;
+
+                    len = XLookupString(&ev.xkey, name, sizeof(name) - 1, &ks, NULL);
+                    if(keyup && ks == XK_Escape) run = false;
+                    if(!keyup && len == 1)
+                    {
+                        if(ui.kbdfocus && (isgraph(name[0]) || name[0] == ' '))
+                            strcat((char*)ui.kbdfocus, name);
+                    }
+                    if(!keyup && ks == XK_BackSpace)
+                    {
+                        if(ui.kbdfocus)
+                        {
+                            ssize i = strlen((char*)ui.kbdfocus) - 1;
+                            if(i >= 0) ((char*)ui.kbdfocus)[i] = '\0';
+                        }
+                    }
+                }
+                break;
+
+                case ButtonPress:
+                    if(ev.xbutton.button == Button1) ui.btns |= bit(1);
+                    if(ev.xbutton.button == Button2) ui.btns |= bit(2);
+                    if(ev.xbutton.button == Button3) ui.btns |= bit(3);
+                    break;
+
+                case ButtonRelease:
+                    if(ev.xbutton.button == Button1) ui.btns &= ~bit(1);
+                    if(ev.xbutton.button == Button2) ui.btns &= ~bit(2);
+                    if(ev.xbutton.button == Button3) ui.btns &= ~bit(3);
+                    break;
+
+                case MotionNotify:
+                    ui.x = ev.xmotion.x;
+                    ui.y = ev.xmotion.y;
+                    break;
+
+                case Expose:
+                {
+                    XWindowAttributes wa = {0};
+                    XGCValues gcvals = {0};
+
+                    gcvals.line_width = 1;
+                    gcvals.foreground = x11.cols.as.bg;
+
+                    XGetWindowAttributes(x11.disp, x11.wnd, &wa);
+
+                    XChangeGC(x11.disp, x11.gc, GCForeground | GCLineWidth, &gcvals);
+                    XFillRectangle(x11.disp, x11.wnd, x11.gc, 0, 0, wa.width, wa.height);
+
+                    /* TODO: draw ui
+                     * for ui draw cmds
+                     *  draw cmd
+                     */
+                }
+                break;
+
+                default:
+                    break;
+            }
+        }
+
+        {
+            int len = 0;
+            char buf[512] = {0};
+            XGCValues gcvals = {0};
+
+            gcvals.foreground = x11.cols.as.bg;
+            XChangeGC(x11.disp, x11.gc, GCForeground | GCLineWidth, &gcvals);
+            XFillRectangle(x11.disp, x11.wnd, x11.gc, 0, 0, 800, 600);
+
+            XSetForeground(x11.disp, x11.gc, x11.cols.as.fg);
+            len = snprintf(buf, lengthof(buf), "hot: %p", (void*)ui.hot);
+            XDrawString(x11.disp, x11.wnd, x11.gc, 650, 25, buf, len);
+
+            len = snprintf(buf, lengthof(buf), "act: %p", (void*)ui.active);
+            XDrawString(x11.disp, x11.wnd, x11.gc, 650, 45, buf, len);
+
+            len = snprintf(buf, lengthof(buf), "cur: %dx%d", ui.x, ui.y);
+            XDrawString(x11.disp, x11.wnd, x11.gc, 650, 65, buf, len);
+
+            len = snprintf(buf, lengthof(buf), "btn: %d%d%d",
+                    ui.btns & bit(1) ? 1 : 0,
+                    ui.btns & bit(2) ? 2 : 0,
+                    ui.btns & bit(3) ? 3 : 0);
+            XDrawString(x11.disp, x11.wnd, x11.gc, 650, 85, buf, len);
+
+            len = snprintf(buf, lengthof(buf), "old: %d%d%d",
+                    ui.pbtns & bit(1) ? 1 : 0,
+                    ui.pbtns & bit(2) ? 2 : 0,
+                    ui.pbtns & bit(3) ? 3 : 0);
+            XDrawString(x11.disp, x11.wnd, x11.gc, 650, 105, buf, len);
+
+            len = snprintf(buf, lengthof(buf), "kbd: %p", (void*)ui.kbdfocus);
+            XDrawString(x11.disp, x11.wnd, x11.gc, 650, 125, buf, len);
+        }
+
+        iui_start(&ui, &x11);
+            if(iui_button(&ui, 25, 25, 100, 35, "Click ME!"))  report(DBG, "CLICK!\n");
+            if(iui_button(&ui, 150, 25, 100, 35, "NOT ME!!!")) report(DBG, "WHYYYYYY!\n");
+
+            iui_textbox(&ui, 25, 75, 225, 35, txtbuf, lengthof(txtbuf));
+            if(iui_button(&ui, 275, 75, 100, 35, "OK")) report(DBG, "textbox: '%s'!\n", txtbuf);
+        iui_end(&ui);
+
+        usleep(10000);
+    }
+
+    x11_cleanup(&x11);
+    return 0;
+}
+
+int main2(int argc, char** argv)
 {
     u64   sz = 0;
     FILE*  f = NULL;
@@ -93,6 +433,7 @@ int main(int argc, char** argv)
     {
         char buf[512] = {0};
 
+
         c8_disasm(c8.RAM, c8.pc, buf);
         printf("%04X:   %s\n", c8.pc, buf);
 
diff --git a/sources/tga.c b/sources/tga.c
new file mode 100644
index 0000000..ccdd64d
--- /dev/null
+++ b/sources/tga.c
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 dwlr <dweller@cabin.digital>
+ *
+ * BSD 3-Clause License (BSD-3-Clause)
+ * See LICENSE for details
+ */
+
+typedef struct packed tga_head
+{
+    u8 id_len;
+    u8 cmap_type;
+    u8 img_type;
+
+    struct packed
+    {
+        u16 first_entry_idx;
+        u16 length;
+        u8  entry_size;
+
+    } cmap_spec;
+
+    struct packed
+    {
+        u16 xorig;
+        u16 yorig;
+        u16 width;
+        u16 height;
+        u8  depth;
+        u8  desc;
+
+    } img_spec;
+
+} tga_head;
+
+typedef enum tga_img_type
+{
+    TGA_IMG_NONE,
+    TGA_IMG_RAW_CMAP,
+    TGA_IMG_RAW_TRUE,
+    TGA_IMG_RAW_BW,
+    TGA_IMG_RLE_CMAP,
+    TGA_IMG_RLE_TRUE,
+    TGA_IMG_RLE_BW,
+
+    TGA_IMG_TYPE_SIZE
+
+} tga_img_type;
+
+typedef union rgba
+{
+    u32 rgba;
+    struct
+    {
+        u8 b;
+        u8 g;
+        u8 r;
+        u8 a;
+    } c;
+
+} rgba;
+
+#define A c.a
+#define R c.r
+#define G c.g
+#define B c.b
+
+
+typedef struct texture
+{
+    ssize width;
+    ssize height;
+
+    rgba* texels;
+
+} texture;
+
+
+/* FIXME: return 1x1 magenta tex on fail? */
+bool tga2tex_from_mem(texture* tex, const u8* data)
+{
+    tga_head* hdr = NULL;
+    rgba*     raw = NULL;
+    usize     sz  = 0;
+
+    assert(tex);
+    assert(data);
+
+    hdr = (tga_head*)data;
+    if(hdr->img_type != TGA_IMG_RAW_TRUE) return false;
+    if(hdr->cmap_type) return false;
+
+    /* FIXME: check pixel depth */
+    /* FIXME: check bit 5 and 4 of img_spec.desc (ordering) */
+
+    tex->width  = hdr->img_spec.width;
+    tex->height = hdr->img_spec.height;
+
+    sz = sizeof(tex->texels[0]) * tex->width * tex->height;
+    tex->texels = malloc(sz);
+    if(!tex->texels) return false;
+
+    raw = (rgba*)(((u8*)hdr) + sizeof(*hdr) + hdr->id_len);
+
+    /* FIXME: convert if needed */
+    /* XXX: possibly misaligned */
+    memcpy(tex->texels, raw, sz);
+
+    return true;
+}
+
+
+bool tga2tex_from_file(texture* tex, const char* path)
+{
+    struct stat stat = {0};
+
+    bool   ret  = false;
+    s32    fd   = -1;
+    u8*    file = NULL;
+
+    assert(tex);
+    assert(path);
+
+    fd = open(path, O_RDONLY);
+    if(fd < 0) goto clean_close;
+
+    if(fstat(fd, &stat) != 0) goto clean_close;
+
+    file = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+    if(file == MAP_FAILED) goto clean_close;
+
+    ret = tga2tex_from_mem(tex, file);
+
+    munmap(file, stat.st_size);
+clean_close:
+    close(fd);
+
+    return ret;
+}
diff --git a/sources/x11.c b/sources/x11.c
new file mode 100644
index 0000000..b9f27b3
--- /dev/null
+++ b/sources/x11.c
@@ -0,0 +1,294 @@
+#if true != True
+    #error "true != Xlib's True"
+#endif
+
+#if false != False
+    #error "false != Xlib's False"
+#endif
+
+#define RGB(r, g, b) (((r) << 16) | ((g) << 8) | (b))
+
+static const char icon_tga[] =
+{
+    #include "../resources/icon.tga.h"
+};
+
+
+typedef struct xctx
+{
+    Display* disp;
+    int      scrn; /* X screen */
+    Window   root;
+    Window   wnd;
+    GC       gc;   /* Not sure if I wanna keep just one GC, but works ok on modern linux */
+    XFontStruct* xfs;
+    XrmDatabase xrdb;
+    Atom xa_delwnd;
+
+    union
+    {
+        struct
+        {
+            u32 fg;
+            u32 bg;
+            u32 col[16];
+
+        } as;
+
+        u32 arr[18];
+
+    } cols;
+
+} xctx;
+
+
+static int x11_err_handler(Display* disp, XErrorEvent* eev)
+{
+    char buf[512] = {0};
+
+    assert(disp);
+    assert(eev);
+
+    XGetErrorText(disp, eev->error_code, buf, sizeof(buf) - 1);
+
+    report(ERR, "X11: '%s' resource - %X; opcode %d(%d)\n",
+            buf, eev->resourceid, eev->request_code, eev->minor_code);
+
+    return 0;
+}
+
+
+static char ascii_lower(char c)
+{
+    if(c >= 'A' && c <= 'Z') return c + ('a' - 'A');
+    else                     return c;
+}
+
+/* TODO: write tests maybe? */
+/* TODO: parse #rgb */
+/* TODO: parse X11 color names */
+static long parse_color(const char* s, long len)
+{
+    long col = 0;
+
+    if(!s || len == 0)
+    {
+        report(WARN, "parse_color: invalid input");
+        goto fail;
+    }
+
+    if(s[0] == '#')
+    {
+        long i;
+
+        if(len < 7)
+        {
+            goto fail;
+            report(WARN, "parse_color: bad length");
+        }
+
+        for(i = 1; i < len; i++)
+        {
+            char c = ascii_lower(s[i]);
+            char r = 0;
+
+            if(c >= '0' && c <= '9')      r = c - '0';
+            else if(c >= 'a' && c <= 'f') r = c - 'a' + 10;
+            else
+            {
+                goto fail;
+                report(WARN, "parse_color: bad hexcode");
+            }
+
+            col |= r << (20 - (i - 1)*4);
+        }
+    }
+    else goto fail;
+
+    return col;
+fail:
+    return RGB(255, 0, 255);
+}
+
+static void x11_init(int width, int height, char* title, int argc, char** argv, xctx* ctx)
+{
+    if(!ctx) report(FATAL, "x11_init: ctx == NULL");
+
+    ctx->disp = XOpenDisplay(NULL);
+    if(!ctx->disp) report(FATAL, "XOpenDisplay");
+
+    XSetErrorHandler(x11_err_handler);
+
+    ctx->scrn = DefaultScreen(ctx->disp);
+    ctx->root = RootWindow(ctx->disp, ctx->scrn);
+
+    {
+        int      xver = 0;
+        int      xrev = 0;
+        int      xrel = 0;
+        char* xvendor = NULL;
+
+        xver = XProtocolVersion(ctx->disp);
+        xrev = XProtocolRevision(ctx->disp);
+
+        xvendor = XServerVendor(ctx->disp);
+        xrel    = XVendorRelease(ctx->disp);
+
+        report(INFO, "X11: Connected to X server '%s' %d\n", xvendor, xrel);
+        report(INFO, "X11: Protocol ver %d.%d\n", xver, xrev);
+        report(INFO, "X11: Screen %d %dx%d\n", ctx->scrn,
+                     DisplayWidth(ctx->disp,  ctx->scrn),
+                     DisplayHeight(ctx->disp, ctx->scrn));
+    }
+
+    /* TODO: make cfg_load() or something that loads the colorscheme */
+    {
+        int i = 0;
+        char* rcmgr = NULL;
+        const long defcols[16] =
+        {
+            RGB(64,64,64),
+            RGB(128,0,0),
+            RGB(0,128,0),
+            RGB(128,128,0),
+            RGB(0,0,128),
+            RGB(128,0,128),
+            RGB(0,128,128),
+            RGB(220,220,220),
+            RGB(210,210,210),
+            RGB(255,0,0),
+            RGB(0,255,0),
+            RGB(255,255,0),
+            RGB(0,0,255),
+            RGB(255,0,255),
+            RGB(0,255,255),
+            RGB(255,255,255)
+        };
+
+
+        ctx->cols.as.fg = RGB(0, 0, 0);
+        ctx->cols.as.bg = RGB(255, 255, 255);
+        for(i = 0; i < 16; i++) ctx->cols.arr[i+2] = defcols[i];
+
+        XrmInitialize();
+        rcmgr = XResourceManagerString(ctx->disp);
+        if(rcmgr && (ctx->xrdb = XrmGetStringDatabase(rcmgr)))
+        {
+            XrmValue val = {0};
+            char*  type = NULL;
+
+            if(XrmGetResource(ctx->xrdb, "foreground", "Foreground", &type, &val))
+                ctx->cols.as.fg = parse_color(val.addr, val.size - 1);
+
+            if(XrmGetResource(ctx->xrdb, "background", "Background", &type, &val))
+                ctx->cols.as.bg = parse_color(val.addr, val.size - 1);
+
+            for(i = 0; i < 16; i++)
+            {
+                char name[8]  = {0};
+                char class[8] = {0};
+                snprintf(name, 8, "color%d", i);
+                strcpy(class, name);
+                class[0] = 'C';
+
+                if(XrmGetResource(ctx->xrdb, name, class, &type, &val))
+                    ctx->cols.arr[i+2] = parse_color(val.addr, val.size - 1);
+            }
+        }
+        else report(WARN, "X11: failed to load X Resources database\n");
+    }
+
+    ctx->wnd = XCreateSimpleWindow(ctx->disp, ctx->root,
+                    0, 0, width, height, 0,
+                    BlackPixel(ctx->disp, ctx->scrn), WhitePixel(ctx->disp, ctx->scrn));
+
+    XSelectInput(ctx->disp, ctx->wnd,
+                  KeyPressMask    | KeyReleaseMask    | KeymapStateMask
+                | ButtonPressMask | ButtonReleaseMask | PointerMotionMask
+                | FocusChangeMask | ExposureMask      | StructureNotifyMask
+                );
+
+    {
+        Atom     xa_wnd_type = {0};
+        Atom xa_wnd_types[3] = {0};
+        Atom         xa_icon = {0};
+
+        XClassHint class = {0};
+        XSizeHints  size = {0};
+
+        texture icon_tex = {0};
+        ssize    icon_sz = 0;
+        long*       icon = NULL;
+
+        char    buf[512] = {0};
+
+
+        ctx->xa_delwnd  = XInternAtom(ctx->disp, "WM_DELETE_WINDOW", false);
+        xa_wnd_type     = XInternAtom(ctx->disp, "_NET_WM_WINDOW_TYPE", false);
+        xa_wnd_types[0] = XInternAtom(ctx->disp, "_NET_WM_WINDOW_TYPE_UTILITY", false);
+        xa_wnd_types[1] = XInternAtom(ctx->disp, "_NET_WM_WINDOW_TYPE_DIALOG",  false);
+        xa_wnd_types[2] = XInternAtom(ctx->disp, "_NET_WM_WINDOW_TYPE_NORMAL",  false);
+        xa_icon         = XInternAtom(ctx->disp, "_NET_WM_ICON",  false);
+
+
+        class.res_name  = title;
+
+        /* FIXME: add class arg */
+        snprintf(buf, sizeof(buf) - 1, "digital.cabin.%s", title);
+        class.res_class = buf;
+
+        size.flags  = PSize | PMinSize | PMaxSize;
+        size.width  = size.min_width  = size.max_width  = width;
+        size.height = size.min_height = size.max_height = height;
+
+        Xutf8SetWMProperties(ctx->disp, ctx->wnd, title, class.res_name, argv, argc, &size, NULL, &class);
+        XSetWMProtocols(ctx->disp, ctx->wnd, &ctx->xa_delwnd, 1);
+        XChangeProperty(ctx->disp, ctx->wnd, xa_wnd_type, XA_ATOM, 32, PropModeReplace,
+                        (u8*)xa_wnd_types, lengthof(xa_wnd_types));
+
+        /* FIXME: add u8[] icon arg */
+        if(tga2tex_from_mem(&icon_tex, (u8*)icon_tga))
+        {
+            icon_sz = 2 + icon_tex.width * icon_tex.height;
+            icon = malloc(sizeof(long) * icon_sz);
+            if(icon)
+            {
+                u32 x, y, y2;
+                icon[0] = icon_tex.width;
+                icon[1] = icon_tex.height;
+
+                for(y = 0, y2 = icon_tex.height - 1; y < icon_tex.height; y++, y2--)
+                    for(x = 0; x < icon_tex.width; x++)
+                        (icon + 2)[x + icon_tex.height * y] =
+                            icon_tex.texels[x + icon_tex.height * y2].rgba;
+
+                XChangeProperty(ctx->disp, ctx->wnd, xa_icon, XA_CARDINAL, 32, PropModeReplace,
+                        (u8*)icon, icon_sz);
+
+                free(icon);
+            }
+            else report(ERR, "Could not allocate X ICON temporary buffer");
+
+            free(icon_tex.texels);
+        }
+        else report(ERR, "Could not read embedded icon.tga");
+    }
+
+    ctx->gc  = XCreateGC(ctx->disp, ctx->wnd, 0, NULL);
+    ctx->xfs = XQueryFont(ctx->disp, XGContextFromGC(ctx->gc)); /* TODO: better font support */
+
+    XClearWindow(ctx->disp, ctx->wnd);
+    XMapWindow(ctx->disp, ctx->wnd);
+}
+
+static void x11_cleanup(xctx* ctx)
+{
+    if(!ctx) return;
+
+    XSetCloseDownMode(ctx->disp, DestroyAll);
+
+    XFreeFontInfo(NULL, ctx->xfs, 1);
+    XFreeGC(ctx->disp, ctx->gc);
+    XDestroyWindow(ctx->disp, ctx->wnd);
+    XCloseDisplay(ctx->disp);
+}
diff --git a/utils/bin2.c b/utils/bin2.c
new file mode 100755
index 0000000..81a1ada
--- /dev/null
+++ b/utils/bin2.c
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 dwlr <dweller@cabin.digital>
+ *
+ * BSD 3-Clause License (BSD-3-Clause)
+ * See LICENSE for details
+ */
+
+
+#define _XOPEN_SOURCE 500
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+
+
+int main(int argc, char** argv)
+{
+    int   rc  = 0;
+    int   ret = EXIT_SUCCESS;
+    FILE* in  = stdin;
+    FILE* out = stdout;
+    const char* name  = "stdin";
+    char buffer[4096] = {0};
+    size_t got = 0;
+
+    if(argc == 2)
+    {
+        name = argv[1];
+        in = fopen(name, "r");
+        if(!in)
+        {
+            perror("fopen");
+            exit(EXIT_FAILURE);
+        }
+
+        rc = snprintf(buffer, sizeof(buffer) - 1, "%s.h", name);
+        if(rc < 0)
+        {
+            perror("snprintf");
+            exit(EXIT_FAILURE);
+        }
+
+        out = fopen(buffer, "w");
+        if(!out)
+        {
+            perror("fopen");
+            fclose(in);
+            exit(EXIT_FAILURE);
+        }
+
+    }
+    else if(argc > 2) fprintf(stderr, "warning: ignoring excess paramters\n");
+
+    for(;;)
+    {
+        size_t i;
+
+        got = fread(buffer, 1, sizeof(buffer), in);
+        rc  = ferror(in);
+        if(rc)
+        {
+            perror("fread");
+            ret = EXIT_FAILURE;
+            goto end;
+        }
+
+        for(i = 0; i < got; i++) fprintf(out, "'\\x%02x',", (unsigned char)buffer[i]);
+
+        rc = feof(in);
+        if(rc) break;
+    }
+
+    fprintf(out, "\n");
+
+end:
+    fclose(in);
+    fclose(out);
+    return ret;
+}
-- 
cgit v1.2.3