alarm-tegra-x1 / device-tools/jcosk.c

raw file 16729 bytes
/*
 * jcosk - Horizon OS-style on-screen keyboard for the ALARM Switch build.
 *
 * Toggled by jcmapper on Screenshot + R3. Draws an override-redirect window
 * docked to the bottom of the screen and injects real key events via XTest,
 * so any focused application receives them normally.
 *
 * XTest rather than XSendEvent because most toolkits ignore synthetic key
 * events, while XTest goes through the server input path like hardware.
 *
 * Rendered with cairo rather than Xft: rounded keys are the defining visual
 * trait of the Horizon keyboard and cairo gives us them for free, plus its
 * toy font API talks straight to fontconfig so we need no freetype include
 * path (pkg-config is absent on this rootfs).
 *
 * Layout follows the real Switch keyboard: a 12-column grid with no
 * row stagger, a rearranged top symbol row, one Shift key, a ?123/ABC
 * page switch, a text echo field and an OK button.
 *
 * Build: gcc -O2 -Wall -Wextra -o jcosk jcosk.c -lX11 -lXtst -lcairo
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <unistd.h>
#include <sys/select.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/XKBlib.h>
#include <X11/keysym.h>
#include <X11/extensions/XTest.h>
#include <cairo/cairo.h>
#include <cairo/cairo-xlib.h>

#define UNITS   12      /* key-width units per row, no stagger */
#define GAP     5
#define MARGIN  8
#define KEYH    58
#define HEADER  44
#define RADII   9       /* corner radius, px */
#define FONTPX  24
#define SPECIAL 16      /* font px for Shift/Ctrl/etc */

typedef struct { const char *label; KeySym ks; int units; } KeyDef;

/* ---- page 0: letters -------------------------------------------------- */
static const KeyDef L0[] = {
    {"1",XK_1,1},{"2",XK_2,1},{"3",XK_3,1},{"4",XK_4,1},{"5",XK_5,1},
    {"6",XK_6,1},{"7",XK_7,1},{"8",XK_8,1},{"9",XK_9,1},{"0",XK_0,1},
    {"-",XK_minus,1},{"=",XK_equal,1}
};
static const KeyDef L1[] = {
    {"q",XK_q,1},{"w",XK_w,1},{"e",XK_e,1},{"r",XK_r,1},{"t",XK_t,1},
    {"y",XK_y,1},{"u",XK_u,1},{"i",XK_i,1},{"o",XK_o,1},{"p",XK_p,1},
    {"[",XK_bracketleft,1},{"]",XK_bracketright,1}
};
static const KeyDef L2[] = {
    {"a",XK_a,1},{"s",XK_s,1},{"d",XK_d,1},{"f",XK_f,1},{"g",XK_g,1},
    {"h",XK_h,1},{"j",XK_j,1},{"k",XK_k,1},{"l",XK_l,1},{";",XK_semicolon,1},
    {"'",XK_apostrophe,1},{"Bksp",XK_BackSpace,1}
};
static const KeyDef L3[] = {
    {"Shift",XK_Shift_L,1},{"z",XK_z,1},{"x",XK_x,1},{"c",XK_c,1},
    {"v",XK_v,1},{"b",XK_b,1},{"n",XK_n,1},{"m",XK_m,1},{",",XK_comma,1},
    {".",XK_period,1},{"/",XK_slash,1},{"Enter",XK_Return,1}
};
static const KeyDef L4[] = {
    {"Esc",XK_Escape,1},{"Ctrl",XK_Control_L,1},{"Alt",XK_Alt_L,1},
    {"Space",XK_space,3},{"\xe2\x86\x90",XK_Left,1},{"\xe2\x86\x92",XK_Right,1},
    {"\xe2\x86\x91",XK_Up,1},{"\xe2\x86\x93",XK_Down,1},{"OK",XK_VoidSymbol,2}
};

/* ---- page 1: symbols -------------------------------------------------- */
static const KeyDef S0[] = {
    {"!",XK_exclam,1},{"@",XK_at,1},{"#",XK_numbersign,1},{"$",XK_dollar,1},
    {"%",XK_percent,1},{"^",XK_asciicircum,1},{"&",XK_ampersand,1},
    {"*",XK_asterisk,1},{"(",XK_parenleft,1},{")",XK_parenright,1},
    {"_",XK_underscore,1},{"+",XK_plus,1}
};
static const KeyDef S1[] = {
    {"`",XK_grave,1},{"~",XK_asciitilde,1},{"\\",XK_backslash,1},
    {"|",XK_bar,1},{"[",XK_bracketleft,1},{"]",XK_bracketright,1},
    {"{",XK_braceleft,1},{"}",XK_braceright,1},{"<",XK_less,1},
    {">",XK_greater,1},{"?",XK_question,1},{":",XK_colon,1}
};
static const KeyDef S2[] = {
    {"\"",XK_quotedbl,1},{";",XK_semicolon,1},{"'",XK_apostrophe,1},
    {",",XK_comma,1},{".",XK_period,1},{"/",XK_slash,1},
    {"*",XK_asterisk,1},{"(",XK_parenleft,1},{")",XK_parenright,1},
    {"=",XK_equal,1},{"+",XK_plus,1},{"Bksp",XK_BackSpace,1}
};

static const KeyDef *page0[5] = { L0, L1, L2, L3, L4 };
static const KeyDef *page1[5] = { S0, S1, S2, L3, L4 };
static const int page0_n[5] = {
    (int)(sizeof L0 / sizeof L0[0]), (int)(sizeof L1 / sizeof L1[0]),
    (int)(sizeof L2 / sizeof L2[0]), (int)(sizeof L3 / sizeof L3[0]),
    (int)(sizeof L4 / sizeof L4[0])
};
static const int page1_n[5] = {
    (int)(sizeof S0 / sizeof S0[0]), (int)(sizeof S1 / sizeof S1[0]),
    (int)(sizeof S2 / sizeof S2[0]), (int)(sizeof L3 / sizeof L3[0]),
    (int)(sizeof L4 / sizeof L4[0])
};

/* runtime key geometry + resolved keycodes */
typedef struct {
    int x, y, w, h;
    const char *label;
    KeyCode kc;
    KeySym ks;
    int special;    /* 1 Shift, 2 Ctrl, 3 Alt, 4 OK */
} RKey;

static RKey keys[64];
static int nkeys = 0;

static Display *dpy;
static Window win;
static int W, H;
static Visual *vis;
static cairo_surface_t *surf;
static cairo_t *cr;
static int page = 0;

/* header hit zones */
static int pg_x, pg_w, ok_x, ok_w;
static int hdr_y, hdr_h;

/* text echo of what we have injected */
static char echo[256];
static int echon = 0;

/* one-shot modifier state */
static int shift_on = 0, ctrl_on = 0, alt_on = 0;
static KeyCode shift_kc, ctrl_kc, alt_kc;
static int running = 1;

static void die(const char *m) { fprintf(stderr, "jcosk: %s\n", m); exit(1); }

/*
 * Set the cairo source from "#rrggbb".
 *
 * The leading '#' must be skipped: "%x" skips whitespace but not '#', so
 * sscanf on the raw string returns 0 and the source is silently left
 * unchanged. That produced an all-black window while geometry (and therefore
 * hit-testing) kept working, which was very hard to spot. Warn loudly if it
 * ever fails again.
 */
static void set_rgb(const char *hex)
{
    unsigned int r, g, b;

    if (*hex == '#') hex++;
    if (sscanf(hex, "%02x%02x%02x", &r, &g, &b) != 3) {
        fprintf(stderr, "jcosk: bad color '%s'\n", hex);
        return;
    }
    cairo_set_source_rgb(cr, r / 255.0, g / 255.0, b / 255.0);
}

/* rounded rectangle path */
static void rrect(double x, double y, double w, double h, double r)
{
    double rr = r;
    if (rr > w / 2) rr = w / 2;
    if (rr > h / 2) rr = h / 2;
    cairo_new_sub_path(cr);
    cairo_arc(cr, x + w - rr, y + rr,       rr, -1.5707963, 0);
    cairo_arc(cr, x + w - rr, y + h - rr,   rr,  0,          1.5707963);
    cairo_arc(cr, x + rr,     y + h - rr,   rr,  1.5707963,  3.1415927);
    cairo_arc(cr, x + rr,     y + rr,       rr,  3.1415927,  4.7123890);
    cairo_close_path(cr);
}

static void fill_rrect(double x, double y, double w, double h, double r,
                       const char *hex)
{
    set_rgb(hex);
    rrect(x, y, w, h, r);
    cairo_fill(cr);
}

static void text_centered(const char *s, double x, double y, double w, double h,
                          double px, const char *hex, int bold)
{
    cairo_text_extents_t e;

    cairo_set_font_size(cr, px);
    cairo_select_font_face(cr, "FreeSans",
                           CAIRO_FONT_SLANT_NORMAL,
                           bold ? CAIRO_FONT_WEIGHT_BOLD : CAIRO_FONT_WEIGHT_NORMAL);
    cairo_text_extents(cr, s, &e);
    set_rgb(hex);
    cairo_move_to(cr,
                  x + (w - e.width) / 2 - e.x_bearing,
                  y + (h - e.height) / 2 - e.y_bearing);
    cairo_show_text(cr, s);
}

static void fake(KeyCode kc, int down)
{
    if (!kc) return;
    XTestFakeKeyEvent(dpy, kc, down, 0);
}

/*
 * Send a key with any one-shot modifiers held.
 *
 * Shift is also held automatically when the keysym only exists on the
 * shifted level of its keycode - otherwise XTest would send the unshifted
 * character and "!" would come out as "1".
 */
static void send_key(RKey *k)
{
    int hold_shift, hold_ctrl, hold_alt;

    /* modifier / OK keys just change state */
    if (k->special == 1) { shift_on = !shift_on; return; }
    if (k->special == 2) { ctrl_on  = !ctrl_on;  return; }
    if (k->special == 3) { alt_on   = !alt_on;   return; }
    if (k->special == 4) { running  = 0;         return; }

    hold_shift = shift_on;
    hold_ctrl  = ctrl_on;
    hold_alt   = alt_on;

    if (k->kc && k->ks != XK_VoidSymbol) {
        KeySym s0 = XkbKeycodeToKeysym(dpy, k->kc, 0, 0);
        KeySym s1 = XkbKeycodeToKeysym(dpy, k->kc, 0, 1);
        if (s0 != k->ks && s1 == k->ks) hold_shift = 1;
    }

    if (hold_shift) fake(shift_kc, 1);
    if (hold_ctrl)  fake(ctrl_kc, 1);
    if (hold_alt)   fake(alt_kc, 1);
    fake(k->kc, 1);
    fake(k->kc, 0);
    if (hold_alt)   fake(alt_kc, 0);
    if (hold_ctrl)  fake(ctrl_kc, 0);
    if (hold_shift) fake(shift_kc, 0);
    XFlush(dpy);

    /* keep the echo field in step with what we sent */
    if (k->ks == XK_BackSpace) {
        if (echon) echo[--echon] = '\0';
    } else if (k->ks == XK_Return) {
        echon = 0; echo[0] = '\0';
    } else if (k->ks == XK_space) {
        if (echon < (int)sizeof echo - 1) echo[echon++] = ' ', echo[echon] = '\0';
    } else if (k->label && strlen(k->label) == 1 && echon < (int)sizeof echo - 1) {
        char c = k->label[0];
        if (shift_on && c >= 'a' && c <= 'z') c -= 32;   /* reflect shift */
        echo[echon++] = c;
        echo[echon] = '\0';
    }

    shift_on = ctrl_on = alt_on = 0;   /* one-shot consumed */
}

static void redraw(void)
{
    const KeyDef **rows;
    const int *rnc;
    int r, i, x, y;
    int avail, unitw;

    cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
    set_rgb("#171a21");
    cairo_paint(cr);
    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);

    /* header: page switch | text echo | OK */
    hdr_y = 6;
    hdr_h = HEADER - 12;
    pg_x  = MARGIN;
    pg_w  = 88;
    ok_w  = 76;
    ok_x  = W - MARGIN - ok_w;

    fill_rrect(pg_x, hdr_y, pg_w, hdr_h, 6, "#2b313c");
    text_centered(page ? "ABC" : "?123", pg_x, hdr_y, pg_w, hdr_h,
                  17, "#c9d2e0", 1);

    {
        int fx = pg_x + pg_w + 8;
        int fw = ok_x - 8 - fx;
        cairo_text_extents_t e;

        set_rgb("#10131a");
        rrect(fx, hdr_y, fw, hdr_h, 6);
        cairo_fill(cr);

        cairo_set_font_size(cr, 19);
        cairo_select_font_face(cr, "FreeSans", CAIRO_FONT_SLANT_NORMAL,
                               CAIRO_FONT_WEIGHT_NORMAL);
        set_rgb("#eef1f6");

        /* right-align the echo so the caret sits at the end */
        cairo_text_extents(cr, echo, &e);
        if (e.width > fw - 16) {
            /* trim from the left until it fits */
            const char *p = echo;
            while (*p) {
                cairo_text_extents(cr, p, &e);
                if (e.width <= fw - 16) break;
                p++;
            }
            cairo_move_to(cr, fx + 8, hdr_y + (hdr_h + e.height) / 2 - e.y_bearing);
            cairo_show_text(cr, p);
        } else {
            double cx = fx + fw - 10 - e.width;
            cairo_move_to(cr, cx, hdr_y + (hdr_h + e.height) / 2 - e.y_bearing);
            cairo_show_text(cr, echo);
            /* caret */
            set_rgb("#4f7dff");
            cairo_rectangle(cr, cx + e.width + 2, hdr_y + 9, 2, hdr_h - 18);
            cairo_fill(cr);
        }
    }

    fill_rrect(ok_x, hdr_y, ok_w, hdr_h, 6, "#3f6fd8");
    text_centered("OK", ok_x, hdr_y, ok_w, hdr_h, 18, "#ffffff", 1);

    /* keys: 12 unit columns, gaps between them, no row stagger */
    rows = (page == 0) ? page0 : page1;
    rnc  = (page == 0) ? page0_n : page1_n;

    avail  = W - 2 * MARGIN;
    unitw  = (avail - (UNITS - 1) * GAP) / UNITS;

    nkeys = 0;
    y = HEADER + MARGIN;
    for (r = 0; r < 5; r++) {
        x = MARGIN;
        for (i = 0; i < rnc[r]; i++) {
            const KeyDef *kd = &rows[r][i];
            int kw = kd->units * unitw + (kd->units - 1) * GAP;
            int active = 0;
            const char *hex = "#333944";
            const char *fg  = "#eef1f6";
            double fpx = FONTPX;
            const char *lab = kd->label;
            char upper[2];

            if (nkeys >= 64) break;

            if (kd->ks == XK_Shift_L)   active = shift_on;
            if (kd->ks == XK_Control_L) active = ctrl_on;
            if (kd->ks == XK_Alt_L)     active = alt_on;
            if (kd->ks == XK_VoidSymbol) active = 0;   /* OK */

            if (active) { hex = "#4a5566"; fg = "#ffffff"; }

            fill_rrect(x, y, kw, KEYH, RADII, hex);

            /* modifiers and function keys get a smaller face */
            if (strlen(kd->label) > 1 && kd->ks != XK_VoidSymbol) {
                fpx = SPECIAL;
                hex = active ? "#ffffff" : "#aeb8c8";
                fg  = hex;
            }
            if (kd->ks == XK_VoidSymbol) { fpx = SPECIAL; }

            /* show letters uppercase while one-shot Shift is armed */
            if (shift_on && kd->label[0] >= 'a' && kd->label[0] <= 'z' &&
                kd->label[1] == '\0') {
                upper[0] = kd->label[0] - 32;
                upper[1] = '\0';
                lab = upper;
            }

            text_centered(lab, x, y, kw, KEYH, fpx, fg,
                          kd->ks == XK_VoidSymbol);

            {
                RKey *rk = &keys[nkeys++];
                rk->x = x; rk->y = y; rk->w = kw; rk->h = KEYH;
                rk->label = kd->label;
                rk->ks = kd->ks;
                rk->kc = (kd->ks == XK_VoidSymbol) ? 0
                                                   : XKeysymToKeycode(dpy, kd->ks);
                rk->special = (kd->ks == XK_Shift_L)   ? 1 :
                              (kd->ks == XK_Control_L) ? 2 :
                              (kd->ks == XK_Alt_L)     ? 3 :
                              (kd->ks == XK_VoidSymbol)? 4 : 0;
            }

            x += kw + GAP;
        }
        y += KEYH + GAP;
    }

    cairo_surface_flush(surf);
    XSync(dpy, False);
}

static void handle_button(XButtonEvent *b)
{
    int i;

    if (b->button != 1) return;

    /* header: page switch */
    if (b->y >= hdr_y && b->y < hdr_y + hdr_h &&
        b->x >= pg_x && b->x < pg_x + pg_w) {
        page = !page;
        redraw();
        return;
    }
    /* header: OK dismisses */
    if (b->y >= hdr_y && b->y < hdr_y + hdr_h &&
        b->x >= ok_x && b->x < ok_x + ok_w) {
        running = 0;
        return;
    }
    if (b->y < HEADER) return;

    for (i = 0; i < nkeys; i++) {
        RKey *k = &keys[i];
        if (b->x >= k->x && b->x < k->x + k->w &&
            b->y >= k->y && b->y < k->y + k->h) {
            send_key(k);
            redraw();   /* modifier highlight + echo field may have changed */
            return;
        }
    }
}

int main(void)
{
    int ev, err, maj, min;
    XSetWindowAttributes wa;
    XEvent xev;
    Window root;
    int scr;

    setlocale(LC_ALL, "");

    dpy = XOpenDisplay(NULL);
    if (!dpy) die("cannot open display");
    if (!XTestQueryExtension(dpy, &ev, &err, &maj, &min))
        die("XTest extension unavailable");

    scr  = DefaultScreen(dpy);
    root = RootWindow(dpy, scr);
    vis  = DefaultVisual(dpy, scr);
    W    = DisplayWidth(dpy, scr);

    H = HEADER + MARGIN + 5 * KEYH + 4 * GAP + MARGIN;

    shift_kc = XKeysymToKeycode(dpy, XK_Shift_L);
    ctrl_kc  = XKeysymToKeycode(dpy, XK_Control_L);
    alt_kc   = XKeysymToKeycode(dpy, XK_Alt_L);

    wa.override_redirect = True;
    wa.background_pixel  = BlackPixel(dpy, scr);
    wa.event_mask        = ExposureMask | ButtonPressMask | ButtonReleaseMask;

    win = XCreateWindow(dpy, root, 0, DisplayHeight(dpy, scr) - H,
                        W, H, 0, DefaultDepth(dpy, scr), InputOutput, vis,
                        CWOverrideRedirect | CWBackPixel | CWEventMask, &wa);

    /* never take input focus: keystrokes must reach the app underneath */
    XSetTransientForHint(dpy, win, root);
    {
        XWMHints *h = XAllocWMHints();
        if (h) { h->flags = InputHint; h->input = False; XSetWMHints(dpy, win, h); XFree(h); }
    }

    surf = cairo_xlib_surface_create(dpy, win, vis, W, H);
    cr   = cairo_create(surf);
    if (!cr) die("cairo_create failed");

    XMapWindow(dpy, win);
    XRaiseWindow(dpy, win);
    XSync(dpy, False);

    redraw();
    fprintf(stderr, "jcosk: running %dx%d page=%d\n", W, H, page);

    while (running) {
        while (XPending(dpy)) {
            XNextEvent(dpy, &xev);
            if (xev.type == Expose) redraw();
            else if (xev.type == ButtonPress) handle_button(&xev.xbutton);
        }
        if (!running) break;

        XRaiseWindow(dpy, win);   /* stay above other windows */

        {
            fd_set fds;
            struct timeval tv;
            int xfd = ConnectionNumber(dpy);
            FD_ZERO(&fds);
            FD_SET(xfd, &fds);
            tv.tv_sec = 1; tv.tv_usec = 0;
            select(xfd + 1, &fds, NULL, NULL, &tv);
        }
    }

    fprintf(stderr, "jcosk: exiting\n");
    cairo_destroy(cr);
    cairo_surface_destroy(surf);
    XDestroyWindow(dpy, win);
    XCloseDisplay(dpy);
    return 0;
}