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