/*
* 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;
}