feat(x11): add XDrawer global phone controls drawer, softkey chord detection, and st/T9 scrollback

This commit is contained in:
Luxferre
2026-10-21 09:26:41 +03:00
parent da24f0d9ac
commit 8045929c87
23 changed files with 1265 additions and 55 deletions
+2 -1
View File
@@ -105,6 +105,7 @@ sudo dd if=./images/bananarchy-nokia-argon.img of=/dev/sdX bs=4M status=progress
| :--- | :--- | :--- |
| **Left softkey** | `XF86KbdLcdMenu1` (`KEY_MENU`) | Open **System menu**, close open menu, or wake & unlock when locked |
| **Right softkey / Back** | `XF86KbdLcdMenu2` / `BackSpace` (`KEY_BACK`) | Open **App context menu** (or close menu if open); in apps/T9 preedit acts as `BackSpace` |
| **Both softkeys together** | `KEY_MENU` + `KEY_BACK` | Toggle **`XDrawer`** (`xdrawer`) global phone controls overlay (Speaker, Earpiece, Flashlight, Sound, Microphone, Screen, Keypad) from anywhere |
| **Call (green)** | `XF86PickupPhone` (`KEY_SEND`) | Wake & unlock when locked, confirm (`Return`) in menus, or: **1×** answer call / **App context menu**, **2×** **Focus next window**, **3×** **Toggle tiling layout** (`vertical` -> `horizontal` -> `tabbed`) |
| **End / Power (red)** | `XF86PowerOff` | Wake & unlock when locked, close open menu, or: **1×** **Power menu** (`space` shutter in Megapixels), **2×** cycle screen state, **3×** (or hold `400ms`) launch new terminal |
| **D-Pad up / down** | `Up` / `Down` | Navigate menus/apps; in T9 preedit: toggle letter case (`a` ↔ `A`) or jump `+4` / `-4` candidates; in idle `Ctl` mode: `Page_Up` / `Page_Down` |
@@ -199,7 +200,7 @@ You can switch between **vertical stack (`TTT`, `bstack`)**, **horizontal tile (
| **`4`** | `^R` `^L` `^P` `^N` `^G` |
| **`5`** | `^S` `^Q` `^O` `^T` `^Y` |
| **`6`** | `^B` `^F` `^V` `^H` `^[` `^]` `^^` `^_` |
| **`7`** | `PgUp` `PgDn` `Home` `End` |
| **`7`** | `PgUp` `PgDn` `Home` `End` `ScrlUp` `ScrlDn` |
| **`8`** | `Up` `Down` `Left` `Right` |
| **`9`** | `F1` `F2` `F3` `F4` `F5` `F6` `F7` `F8` `F9` `F10` `F11` `F12` |
| **`0`** | `Space` (`SPC`) `Tab` `Ent` (`Return`) `Esc` |
+1
View File
@@ -11,6 +11,7 @@ Bananarchy targets an ultra-lightweight **Sxmo X11 (`sxmo-de-dwm`)** environment
- **Cross-compiled Xlibre (`X11Libre/xserver` 25.2.0) display server**: `modesetting` KMS + `ShadowFB` Damage partial-rectangle SPI transfers, software cursor fallback, and rootless `elogind` integration.
- **Patched `sxmo-dwm`**: `240x320` vertical `bstack` layout, 24px Pango status bar with an overflow-protected right-aligned clock, and direct C `SW_LID` slider and locked-key wake handling in `< 2 ms`.
- **Patched `sxmo-dmenu`**: `240x320` full-width 9-line vertical menu flush below the 24px top bar with a `#383838` search bar, `#005577` prompt badge, and `XIM` `fcitx5-t9` text input in `< 15 ms`.
- **`XDrawer` global phone controls drawer (`xdrawer`)**: lightweight `240x320` C++17 X11/Pango/ALSA overlay invoked from anywhere by pressing **both softkeys simultaneously**, providing instant sliders and toggles for speaker volume, earpiece volume, flashlight, sound mute, microphone mute, screen brightness, and keypad backlight.
- **Patched `sxmo-st` terminal (`pixelsize=18`)**: **HarfBuzz (`libharfbuzz`) OpenType ligature shaping** (`liga`, `calt`, `clig`, `dlig`) and `XIM` support.
- **Typography**: non-Nerd **`Fira Code` (`font-fira-code`) with ligatures** as the primary system and terminal font (with `DejaVu SansM Nerd Font` used strictly as an icon fallback).
- **Custom 6-mode `fcitx5-t9` input engine**: fixed bottom candidate bar (`X=1, Y=290, W=238, H=30`) with a constant 28-character monospace preedit layout supporting lowercase, one-shot Shift, Caps Lock, all 32 ASCII punctuation symbols, terminal control codes (`^A`-`^Z`, `Esc`, `Tab`, `F1`-`F12`), and direct numeric entry with live top-bar mode indication (`[abc]`, `[Abc]`, `[ABC]`, `[Sym]`, `[Ctl]`, `[123]`).
+1
View File
@@ -0,0 +1 @@
xdrawer
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
xdrawer
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+13 -2
View File
@@ -42,6 +42,7 @@ EOF
alpine-baselayout \
musl-dev \
linux-headers \
alsa-lib-dev \
libx11-dev \
libxft-dev \
libxinerama-dev \
@@ -59,6 +60,7 @@ EOF
export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"
export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig"
CC="armv7-alpine-linux-musleabihf-gcc --sysroot=$SYSROOT"
CXX="armv7-alpine-linux-musleabihf-g++ --sysroot=$SYSROOT"
STRIP="armv7-alpine-linux-musleabihf-strip"
OPT_CFLAGS="-O2 -march=armv7-a -mtune=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard -fno-plt -fomit-frame-pointer"
@@ -95,12 +97,21 @@ EOF
dwm.c drw.c util.c $DWM_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
echo "[6/6] Compiling XDrawer (240x320 global phone controls drawer)..."
cd /bananarchy/src/xdrawer
XDRAWER_INCS="$(pkgconf --cflags x11 xft fontconfig freetype2 pango pangoxft alsa)"
XDRAWER_LIBS="$(pkgconf --libs x11 xft fontconfig freetype2 pango pangoxft alsa)"
$CXX $OPT_CFLAGS -std=c++17 -Wall -Wextra $XDRAWER_INCS \
xdrawer.cpp $XDRAWER_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
ln -sf xdrawer /bananarchy/prebuilt/sxmo-x11/usr/bin/XDrawer
mkdir -p /bananarchy/overlay/rootfs/usr/bin
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/
if [ -d /target-rootfs/usr/bin ]; then
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /target-rootfs/usr/bin/
fi
chown -R "$HOST_UID:$HOST_GID" /bananarchy/prebuilt/sxmo-x11 /bananarchy/overlay/rootfs/usr/bin /bananarchy/src/sxmo-st /bananarchy/src/sxmo-dmenu /bananarchy/src/sxmo-dwm
ls -lh /bananarchy/prebuilt/sxmo-x11/usr/bin/st /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
chown -R "$HOST_UID:$HOST_GID" /bananarchy/prebuilt/sxmo-x11 /bananarchy/overlay/rootfs/usr/bin /bananarchy/src/sxmo-st /bananarchy/src/sxmo-dmenu /bananarchy/src/sxmo-dwm /bananarchy/src/xdrawer
ls -lh /bananarchy/prebuilt/sxmo-x11/usr/bin/st /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
'
+31 -6
View File
@@ -421,6 +421,8 @@ std::vector<T9Item> T9State::getItemsForKey(int keyId) const {
makeKey("PgDn", FcitxKey_Page_Down, 117),
makeKey("Home", FcitxKey_Home, 110),
makeKey("End", FcitxKey_End, 115),
makeKey("ScrlUp", FcitxKey_Page_Up, 112, fcitx::KeyState::Shift),
makeKey("ScrlDn", FcitxKey_Page_Down, 117, fcitx::KeyState::Shift),
};
case kKey8:
return {
@@ -894,12 +896,35 @@ void T9State::showPreview(const std::vector<T9Item>& items, int index) {
if (total == 0) {
return;
}
constexpr int kPageSize = 7;
int page = index / kPageSize;
int start = page * kPageSize;
int end = std::min(start + kPageSize, total);
constexpr int kMaxItemsPerPage = 7;
std::string prefix = modePrefix() + ":";
size_t maxItemsWidth =
(kFixedPreeditChars > prefix.size() + 1)
? (kFixedPreeditChars - prefix.size() - 1)
: 23;
std::string preedit = modePrefix() + ":";
int start = 0;
int end = 0;
while (start < total) {
size_t width = 0;
int count = 0;
end = start;
while (end < total && count < kMaxItemsPerPage) {
size_t itemWidth = items[end].label.size() + 2;
if (count > 0 && width + itemWidth > maxItemsWidth) {
break;
}
width += itemWidth;
++end;
++count;
}
if (index < end) {
break;
}
start = end;
}
std::string preedit = prefix;
for (int i = start; i < end; ++i) {
if (i == index) {
preedit += "[";
@@ -911,7 +936,7 @@ void T9State::showPreview(const std::vector<T9Item>& items, int index) {
preedit += " ";
}
}
if (total > kPageSize) {
if (start > 0 || end < total) {
preedit += (end < total) ? ">" : "<";
}
if (preedit.size() < kFixedPreeditChars) {
+174 -39
View File
@@ -4,6 +4,7 @@
#define _EVDEVK(_v) (0x10081000 + (_v))
#endif
#include <ctype.h>
#include <fcntl.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
@@ -556,11 +557,12 @@ insert:
sel = matchend;
break;
case XK_Escape:
case XF86XK_Back:
case XF86XK_KbdLcdMenu2:
case XF86XK_PowerOff:
cleanup();
exit(1);
case XF86XK_Back:
case XF86XK_KbdLcdMenu2:
return;
case XK_Home:
if (sel == matches) {
cursor = 0;
@@ -610,6 +612,7 @@ insert:
break;
case XK_Menu:
case XF86XK_KbdLcdMenu1:
return;
case XF86XK_Phone:
case XF86XK_PickupPhone:
case XK_Return:
@@ -910,6 +913,8 @@ readstdin(void)
lines = MIN(lines, i);
}
#include <poll.h>
static int
issoftkey(unsigned int keycode)
{
@@ -917,51 +922,181 @@ issoftkey(unsigned int keycode)
return keycode == 124 || keycode == 135 || keycode == 166 || keycode == 177;
}
static long long
dmenu_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000LL + (long long)(ts.tv_nsec / 1000000L);
}
static void
write_softkey_suppress(long long until_ms)
{
char buf[32];
int fd = open("/tmp/sxmo_softkey_suppress", O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd >= 0) {
int len = snprintf(buf, sizeof(buf), "%lld\n", until_ms);
(void)write(fd, buf, len);
close(fd);
}
}
static int
query_softkeys_down(void)
{
char km[32];
int mask = 0;
if (!XQueryKeymap(dpy, km))
return 0;
if (km[135 / 8] & (1 << (135 % 8)))
mask |= 1;
if (km[166 / 8] & (1 << (166 % 8)))
mask |= 2;
return mask;
}
static void
spawn_xdrawer_and_exit(void)
{
write_softkey_suppress(dmenu_now_ms() + 500);
cleanup();
if (fork() == 0) {
int nullfd = open("/dev/null", O_RDWR);
if (nullfd >= 0) {
dup2(nullfd, 0);
dup2(nullfd, 1);
dup2(nullfd, 2);
if (nullfd > 2)
close(nullfd);
}
setsid();
execlp("xdrawer", "xdrawer", (char *)NULL);
execl("/home/user/.local/bin/xdrawer", "xdrawer", (char *)NULL);
execl("/usr/bin/xdrawer", "xdrawer", (char *)NULL);
_exit(0);
}
exit(1);
}
static void
run(void)
{
XEvent ev;
int soft_pending = 0;
long long soft_first_ms = 0;
long long soft_deadline = 0;
long long start_ignore = dmenu_now_ms() + 40;
int initial_released = (query_softkeys_down() == 0);
int xfd = ConnectionNumber(dpy);
while (!XNextEvent(dpy, &ev)) {
if (ev.type == KeyPress || ev.type == KeyRelease) {
if (!issoftkey(ev.xkey.keycode)) {
ev.xkey.window = win;
if (XFilterEvent(&ev, None))
for (;;) {
if (!initial_released && query_softkeys_down() == 0)
initial_released = 1;
while (XPending(dpy)) {
if (XNextEvent(dpy, &ev))
return;
if (!initial_released && query_softkeys_down() == 0)
initial_released = 1;
if (ev.type == KeyPress || ev.type == KeyRelease) {
KeySym ks = XLookupKeysym(&ev.xkey, 0);
int is_left = (ks == XK_Menu || ks == XF86XK_KbdLcdMenu1 || ev.xkey.keycode == 135);
int is_right = (ks == XF86XK_Back || ks == XF86XK_KbdLcdMenu2 || ev.xkey.keycode == 166);
if (is_left || is_right) {
long long now = dmenu_now_ms();
if (ev.type == KeyRelease) {
initial_released = 1;
if (soft_pending != 0 && soft_pending != 3) {
long long grace = now + 65;
if (grace > soft_deadline)
soft_deadline = grace;
}
continue;
}
if (!initial_released || now < start_ignore)
continue;
if (is_left)
soft_pending |= 1;
if (is_right)
soft_pending |= 2;
soft_pending |= query_softkeys_down();
if (soft_pending == 3)
spawn_xdrawer_and_exit();
if (soft_deadline == 0) {
soft_first_ms = now;
soft_deadline = now + 200;
}
continue;
}
if (!issoftkey(ev.xkey.keycode)) {
ev.xkey.window = win;
if (XFilterEvent(&ev, None))
continue;
}
} else if (XFilterEvent(&ev, None)) {
continue;
}
} else if (XFilterEvent(&ev, None)) {
continue;
}
switch(ev.type) {
case MotionNotify:
motionnotify(&ev);
break;
case ButtonRelease:
buttonpress(&ev);
break;
case DestroyNotify:
if (ev.xdestroywindow.window != win)
switch(ev.type) {
case MotionNotify:
motionnotify(&ev);
break;
cleanup();
exit(1);
case Expose:
if (ev.xexpose.count == 0)
drw_map(drw, win, 0, 0, mw, mh);
break;
case FocusIn:
/* regrab focus from parent window */
if (ev.xfocus.window != win)
grabfocus();
if (xic)
XSetICFocus(xic);
break;
case KeyPress:
keypress(&ev.xkey);
break;
case SelectionNotify:
if (ev.xselection.property == utf8)
paste();
break;
case ButtonRelease:
buttonpress(&ev);
break;
case DestroyNotify:
if (ev.xdestroywindow.window != win)
break;
cleanup();
exit(1);
case Expose:
if (ev.xexpose.count == 0)
drw_map(drw, win, 0, 0, mw, mh);
break;
case FocusIn:
/* regrab focus from parent window */
if (ev.xfocus.window != win)
grabfocus();
if (xic)
XSetICFocus(xic);
break;
case KeyPress:
keypress(&ev.xkey);
break;
case SelectionNotify:
if (ev.xselection.property == utf8)
paste();
break;
}
}
if (soft_pending && soft_deadline > 0) {
long long now = dmenu_now_ms();
int hw_down = query_softkeys_down();
soft_pending |= hw_down;
if (soft_pending == 3)
spawn_xdrawer_and_exit();
long long rem = soft_deadline - now;
if (rem <= 0) {
if (hw_down != 0 && (now - soft_first_ms) < 480) {
soft_deadline = now + 25;
rem = 25;
} else {
write_softkey_suppress(now + 400);
if (soft_pending == 1) {
puts((sel) ? sel->text : text);
cleanup();
exit(0);
} else {
cleanup();
exit(1);
}
}
}
struct pollfd pfd = { xfd, POLLIN, 0 };
poll(&pfd, 1, (int)rem);
} else {
struct pollfd pfd = { xfd, POLLIN, 0 };
poll(&pfd, 1, initial_released ? -1 : 40);
}
}
}
+167 -7
View File
@@ -124,7 +124,7 @@ typedef struct {
void (*arrange)(Monitor *);
} Layout;
typedef struct HoldKey {
struct HoldKey {
KeySym keysym;
struct HoldKey *next;
};
@@ -1447,6 +1447,107 @@ ishold(KeySym keysym)
return 0;
}
#define SOFTKEY_LEFT 1
#define SOFTKEY_RIGHT 2
#define SOFTKEY_CHORD_MS 200
#define SOFTKEY_MAX_HOLD_MS 480
#define SOFTKEY_SUPPRESS_MS 500
static int softkey_pending = 0;
static long long softkey_first_ms = 0;
static long long softkey_deadline_ms = 0;
static long long softkey_suppress_until_ms = 0;
static long long
dwm_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000LL + (long long)(ts.tv_nsec / 1000000L);
}
static void
write_softkey_suppress(long long until_ms)
{
char buf[32];
int fd, len;
softkey_suppress_until_ms = until_ms;
fd = open("/tmp/sxmo_softkey_suppress", O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd >= 0) {
len = snprintf(buf, sizeof(buf), "%lld\n", until_ms);
(void)write(fd, buf, len);
close(fd);
}
}
static long long
read_softkey_suppress(void)
{
char buf[32];
read_sysfs_str("/tmp/sxmo_softkey_suppress", buf, sizeof(buf));
long long v = atoll(buf);
if (v > softkey_suppress_until_ms)
softkey_suppress_until_ms = v;
return softkey_suppress_until_ms;
}
static int
query_softkeys_down(void)
{
char km[32];
int mask = 0;
if (!XQueryKeymap(dpy, km))
return 0;
if (km[135 / 8] & (1 << (135 % 8)))
mask |= SOFTKEY_LEFT;
if (km[166 / 8] & (1 << (166 % 8)))
mask |= SOFTKEY_RIGHT;
return mask;
}
static int
is_left_softkey(KeySym ks, unsigned int keycode)
{
return ks == XK_Menu || ks == XF86XK_KbdLcdMenu1 || keycode == 135;
}
static int
is_right_softkey(KeySym ks, unsigned int keycode)
{
return ks == XF86XK_Back || ks == XF86XK_KbdLcdMenu2 || keycode == 166;
}
static void
trigger_xdrawer_chord(long long now)
{
const Arg xdrawercmd = { .v = (const char*[]){ "xdrawer", NULL } };
softkey_pending = 0;
softkey_first_ms = 0;
softkey_deadline_ms = 0;
write_softkey_suppress(now + SOFTKEY_SUPPRESS_MS);
XUngrabKeyboard(dpy, CurrentTime);
XFlush(dpy);
spawn(&xdrawercmd);
}
static void
flush_pending_softkey(void)
{
int pending = softkey_pending;
softkey_pending = 0;
softkey_first_ms = 0;
softkey_deadline_ms = 0;
XUngrabKeyboard(dpy, CurrentTime);
XFlush(dpy);
if (pending == SOFTKEY_LEFT) {
const Arg syscmd = { .v = (const char*[]){ "sxmo_appmenu.sh", "sys", NULL } };
spawn(&syscmd);
} else if (pending == SOFTKEY_RIGHT) {
const Arg ctxcmd = { .v = (const char*[]){ "sxmo_appmenu.sh", NULL } };
spawn(&ctxcmd);
}
}
void
keypress(XEvent *e)
{
@@ -1463,6 +1564,10 @@ keypress(XEvent *e)
keysym == XF86XK_KbdLcdMenu1 || keysym == XF86XK_KbdLcdMenu2) &&
is_phone_locked()) {
const Arg unlockcmd = SHCMD("xset dpms 0 0 0; xset dpms force on; sxmo_state.sh set unlock");
softkey_pending = 0;
softkey_first_ms = 0;
softkey_deadline_ms = 0;
write_softkey_suppress(dwm_now_ms() + SOFTKEY_SUPPRESS_MS);
XUngrabKeyboard(dpy, CurrentTime);
XFlush(dpy);
set_hw_backlight(1);
@@ -1470,6 +1575,28 @@ keypress(XEvent *e)
return;
}
if (CLEANMASK(ev->state) == 0 &&
(is_left_softkey(keysym, ev->keycode) || is_right_softkey(keysym, ev->keycode))) {
long long now = dwm_now_ms();
if (now < read_softkey_suppress()) {
XUngrabKeyboard(dpy, CurrentTime);
XFlush(dpy);
return;
}
if (is_left_softkey(keysym, ev->keycode))
softkey_pending |= SOFTKEY_LEFT;
if (is_right_softkey(keysym, ev->keycode))
softkey_pending |= SOFTKEY_RIGHT;
softkey_pending |= query_softkeys_down();
if (softkey_pending == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
trigger_xdrawer_chord(now);
} else if (softkey_deadline_ms == 0) {
softkey_first_ms = now;
softkey_deadline_ms = now + SOFTKEY_CHORD_MS;
}
return;
}
if (!selmon->sel && CLEANMASK(ev->state) == 0) {
if (keysym == XK_Return) {
const Arg entercmd = { .v = (const char*[]){ "sxmo_appmenu.sh", NULL } };
@@ -1512,6 +1639,15 @@ keyrelease(XEvent *e)
markreleased(keysym);
if (is_left_softkey(keysym, ev->keycode) || is_right_softkey(keysym, ev->keycode)) {
if (softkey_pending != 0 && softkey_pending != (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
long long grace = dwm_now_ms() + 65;
if (grace > softkey_deadline_ms)
softkey_deadline_ms = grace;
}
return;
}
for (i = 0; i < LENGTH(keys); i++) {
if (keysym == keys[i].keysym
&& KeyRelease == keys[i].type
@@ -1953,7 +2089,29 @@ run(void)
fds[1].revents = 0;
nfds = 2;
}
if (poll(fds, nfds, -1) < 0) {
int timeout_ms = -1;
if (softkey_pending && softkey_deadline_ms > 0) {
long long now = dwm_now_ms();
int hw_down = query_softkeys_down();
softkey_pending |= hw_down;
if (softkey_pending == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
trigger_xdrawer_chord(now);
continue;
}
long long rem = softkey_deadline_ms - now;
if (rem <= 0) {
if (hw_down != 0 && (now - softkey_first_ms) < SOFTKEY_MAX_HOLD_MS) {
softkey_deadline_ms = now + 25;
timeout_ms = 25;
} else {
flush_pending_softkey();
continue;
}
} else {
timeout_ms = (int)rem;
}
}
if (poll(fds, nfds, timeout_ms) < 0) {
if (errno == EINTR)
continue;
break;
@@ -1974,10 +2132,12 @@ run(void)
}
}
if (fds[0].revents & (POLLIN | POLLHUP | POLLERR)) {
if (XNextEvent(dpy, &ev))
return;
if (handler[ev.type])
handler[ev.type](&ev);
while (XPending(dpy)) {
if (XNextEvent(dpy, &ev))
return;
if (handler[ev.type])
handler[ev.type](&ev);
}
}
}
}
@@ -2194,7 +2354,7 @@ setup(void)
wa.cursor = cursor[CurNormal]->cursor;
wa.event_mask = SubstructureRedirectMask|SubstructureNotifyMask
|ButtonPressMask|PointerMotionMask|EnterWindowMask
|LeaveWindowMask|StructureNotifyMask|PropertyChangeMask|KeyPressMask;
|LeaveWindowMask|StructureNotifyMask|PropertyChangeMask|KeyPressMask|KeyReleaseMask;
XChangeWindowAttributes(dpy, root, CWEventMask|CWCursor, &wa);
XSelectInput(dpy, root, wa.event_mask);
lidfd = open("/dev/input/by-path/platform-gpio-keys-event", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+2
View File
@@ -239,6 +239,8 @@ static Shortcut shortcuts[] = {
{ TERMMOD, XK_S, toggleselectionmode, {.i = 0} },
{ ShiftMask, XK_Prior, kscrollup, {.i = -1} },
{ ShiftMask, XK_Next, kscrolldown, {.i = -1} },
{ TERMMOD, XK_B, kscrollup, {.i = -1} },
{ TERMMOD, XK_F, kscrolldown, {.i = -1} },
{ TERMMOD, XK_X, invert, { } },
+2
View File
@@ -239,6 +239,8 @@ static Shortcut shortcuts[] = {
{ TERMMOD, XK_S, toggleselectionmode, {.i = 0} },
{ ShiftMask, XK_Prior, kscrollup, {.i = -1} },
{ ShiftMask, XK_Next, kscrolldown, {.i = -1} },
{ TERMMOD, XK_B, kscrollup, {.i = -1} },
{ TERMMOD, XK_F, kscrolldown, {.i = -1} },
{ TERMMOD, XK_X, invert, { } },
+871
View File
@@ -0,0 +1,871 @@
// SPDX-License-Identifier: GPL-2.0-only
// XDrawer - Lightweight X11 global phone controls drawer for Bananarchy (240x320)
#include <X11/Xatom.h>
#include <X11/Xft/Xft.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>
#ifndef _EVDEVK
#define _EVDEVK(_v) (0x10081000 + (_v))
#endif
#include <alsa/asoundlib.h>
#include <pango/pango.h>
#include <pango/pangoxft.h>
#include <algorithm>
#include <cmath>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <poll.h>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
namespace {
constexpr int kScreenWidth = 240;
constexpr int kScreenHeight = 320;
constexpr int kTopBarHeight = 26;
constexpr int kWinX = 0;
constexpr int kWinY = kTopBarHeight;
constexpr int kWinWidth = kScreenWidth;
constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 294px (7 * 42px)
constexpr int kRowCount = 7;
constexpr int kRowHeight = kWinHeight / kRowCount; // 42px per row
// Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font")
constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker
constexpr const char* kIconEarpiece = "\xef\x8a\xa0"; // U+F2A0 earpiece
constexpr const char* kIconFlashlight = "\xf3\xb0\x89\x84"; // U+F0244 flashlight
constexpr const char* kIconSoundOn = "\xf3\xb0\x8e\x88"; // U+F0388 music/sound on
constexpr const char* kIconSoundOff = "\xef\x91\xa6"; // U+F466 mute
constexpr const char* kIconMicOn = "\xf3\xb0\x8d\xac"; // U+F036C mic on
constexpr const char* kIconMicOff = "\xf3\xb0\x8d\xad"; // U+F036D mic off
constexpr const char* kIconBrightness = "\xf3\xb0\x83\x9d"; // U+F00DD brightness
constexpr const char* kIconKeyboard = "\xef\x84\x9c"; // U+F11C keyboard
constexpr const char* kIconToggleOn = "\xef\x88\x85"; // U+F205 toggle on
constexpr const char* kIconToggleOff = "\xef\x88\x84"; // U+F204 toggle off
volatile sig_atomic_t gRunning = 1;
void handleSignal(int) {
gRunning = 0;
}
int64_t nowMs() {
struct timespec ts {};
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<int64_t>(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000;
}
std::string getRuntimeFile(const char* name) {
const char* rdir = std::getenv("XDG_RUNTIME_DIR");
std::string base = (rdir && rdir[0]) ? rdir : "/tmp";
return base + "/" + name;
}
int readIntFile(const std::string& path, int fallback = -1) {
int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return fallback;
}
char buf[64];
ssize_t n = ::read(fd, buf, sizeof(buf) - 1);
::close(fd);
if (n <= 0) {
return fallback;
}
buf[n] = '\0';
return std::atoi(buf);
}
bool writeIntFile(const std::string& path, int value) {
int fd = ::open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, 0644);
if (fd < 0) {
return false;
}
char buf[32];
int len = std::snprintf(buf, sizeof(buf), "%d\n", value);
ssize_t w = ::write(fd, buf, len);
::close(fd);
return w == len;
}
void runAsyncCmd(const std::string& cmd) {
pid_t pid = ::fork();
if (pid == 0) {
::setsid();
int nullfd = ::open("/dev/null", O_RDWR);
if (nullfd >= 0) {
::dup2(nullfd, STDIN_FILENO);
::dup2(nullfd, STDOUT_FILENO);
::dup2(nullfd, STDERR_FILENO);
if (nullfd > 2) {
::close(nullfd);
}
}
execl("/bin/sh", "sh", "-c", cmd.c_str(), static_cast<char*>(nullptr));
_exit(0);
}
}
class AlsaControl {
public:
AlsaControl() {
if (snd_ctl_open(&ctl_, "hw:0", 0) < 0) {
ctl_ = nullptr;
}
}
~AlsaControl() {
if (ctl_) {
snd_ctl_close(ctl_);
}
}
long getInteger(const char* name, long fallback = 0) const {
if (!ctl_) {
return fallback;
}
snd_ctl_elem_id_t* id = nullptr;
snd_ctl_elem_value_t* val = nullptr;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_value_alloca(&val);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, name);
snd_ctl_elem_value_set_id(val, id);
if (snd_ctl_elem_read(ctl_, val) < 0) {
return fallback;
}
return snd_ctl_elem_value_get_integer(val, 0);
}
bool setInteger(const char* name, long v) {
if (!ctl_) {
return false;
}
snd_ctl_elem_id_t* id = nullptr;
snd_ctl_elem_value_t* val = nullptr;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_value_alloca(&val);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, name);
snd_ctl_elem_value_set_id(val, id);
snd_ctl_elem_value_set_integer(val, 0, v);
return snd_ctl_elem_write(ctl_, val) >= 0;
}
bool getBoolean(const char* name, bool fallback = true) const {
if (!ctl_) {
return fallback;
}
snd_ctl_elem_id_t* id = nullptr;
snd_ctl_elem_value_t* val = nullptr;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_value_alloca(&val);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, name);
snd_ctl_elem_value_set_id(val, id);
if (snd_ctl_elem_read(ctl_, val) < 0) {
return fallback;
}
return snd_ctl_elem_value_get_boolean(val, 0) != 0;
}
bool setBoolean(const char* name, bool v) {
if (!ctl_) {
return false;
}
snd_ctl_elem_id_t* id = nullptr;
snd_ctl_elem_value_t* val = nullptr;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_value_alloca(&val);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, name);
snd_ctl_elem_value_set_id(val, id);
snd_ctl_elem_value_set_boolean(val, 0, v ? 1 : 0);
return snd_ctl_elem_write(ctl_, val) >= 0;
}
unsigned int getEnum(const char* name, unsigned int fallback = 0) const {
if (!ctl_) {
return fallback;
}
snd_ctl_elem_id_t* id = nullptr;
snd_ctl_elem_value_t* val = nullptr;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_value_alloca(&val);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, name);
snd_ctl_elem_value_set_id(val, id);
if (snd_ctl_elem_read(ctl_, val) < 0) {
return fallback;
}
return snd_ctl_elem_value_get_enumerated(val, 0);
}
bool setEnum(const char* name, unsigned int v) {
if (!ctl_) {
return false;
}
snd_ctl_elem_id_t* id = nullptr;
snd_ctl_elem_value_t* val = nullptr;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_value_alloca(&val);
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER);
snd_ctl_elem_id_set_name(id, name);
snd_ctl_elem_value_set_id(val, id);
snd_ctl_elem_value_set_enumerated(val, 0, v);
return snd_ctl_elem_write(ctl_, val) >= 0;
}
private:
snd_ctl_t* ctl_ = nullptr;
};
enum class ControlId {
SpeakerVol = 0,
EarpieceVol = 1,
Flashlight = 2,
SoundMute = 3,
MicMute = 4,
ScreenBright = 5,
KbdBright = 6,
};
struct ControlItem {
ControlId id;
const char* iconOn;
const char* iconOff;
std::string label;
bool isSlider;
int minVal;
int maxVal;
int stepVal;
int value;
};
class PhoneController {
public:
PhoneController() {
if (::access("/sys/class/leds/white:torch/brightness", F_OK) == 0) {
torchDir_ = "/sys/class/leds/white:torch";
} else {
torchDir_ = "/sys/class/leds/white:flash";
}
torchMax_ = std::max(1, readIntFile(torchDir_ + "/max_brightness", 255));
backlightDir_ = "/sys/class/backlight/backlight";
screenMax_ = std::max(1, readIntFile(backlightDir_ + "/max_brightness", 14));
kbdDir_ = "/sys/class/leds/white:kbd_backlight";
kbdMax_ = std::max(1, readIntFile(kbdDir_ + "/max_brightness", 1));
kbdSupportsSlider_ = (kbdMax_ > 1);
refreshAll();
}
void refreshAll() {
items_.clear();
// 1. Speaker volume (0..100%, mapped to ALSA RX3 Digital Volume 0..84)
long rx3 = alsa_.getInteger("RX3 Digital Volume", 84);
int spkPct = std::clamp(static_cast<int>(std::lround((rx3 * 100.0) / 84.0)), 0, 100);
spkPct = (spkPct / 5) * 5;
items_.push_back({ControlId::SpeakerVol, kIconSpeaker, kIconSoundOff,
"Speaker", true, 0, 100, 5, spkPct});
// 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 0..84)
long rx1 = alsa_.getInteger("RX1 Digital Volume", 84);
int earPct = std::clamp(static_cast<int>(std::lround((rx1 * 100.0) / 84.0)), 0, 100);
earPct = (earPct / 5) * 5;
items_.push_back({ControlId::EarpieceVol, kIconEarpiece, kIconEarpiece,
"Earpiece", true, 0, 100, 5, earPct});
// 3. Flashlight on/off (toggle)
int torchCur = readIntFile(torchDir_ + "/brightness", 0);
items_.push_back({ControlId::Flashlight, kIconFlashlight, kIconFlashlight,
"Flashlight", false, 0, 1, 1, (torchCur > 0) ? 1 : 0});
// 4. Sound on/off (toggle)
bool spkOn = alsa_.getBoolean("SPK DAC Switch", true);
items_.push_back({ControlId::SoundMute, kIconSoundOn, kIconSoundOff,
"Sound", false, 0, 1, 1, spkOn ? 1 : 0});
// 5. Microphone on/off (toggle)
unsigned int dec1 = alsa_.getEnum("DEC1 MUX", 2);
if (dec1 > 0) {
savedMicMux_ = dec1;
writeIntFile("/tmp/xdrawer_mic_mux", static_cast<int>(savedMicMux_));
} else {
savedMicMux_ = static_cast<unsigned int>(std::max(1, readIntFile("/tmp/xdrawer_mic_mux", 2)));
}
items_.push_back({ControlId::MicMute, kIconMicOn, kIconMicOff,
"Microphone", false, 0, 1, 1, (dec1 > 0) ? 1 : 0});
// 6. Screen brightness (slider 1..screenMax_)
int scrCur = std::clamp(readIntFile(backlightDir_ + "/brightness", screenMax_), 1, screenMax_);
int scrStep = std::max(1, screenMax_ / 14);
items_.push_back({ControlId::ScreenBright, kIconBrightness, kIconBrightness,
"Screen", true, 1, screenMax_, scrStep, scrCur});
// 7. Keyboard brightness (slider if max_brightness > 1, toggle otherwise)
int kbdCur = std::clamp(readIntFile(kbdDir_ + "/brightness", 0), 0, kbdMax_);
if (kbdSupportsSlider_) {
int kbdStep = std::max(1, kbdMax_ / 10);
items_.push_back({ControlId::KbdBright, kIconKeyboard, kIconKeyboard,
"Keypad", true, 0, kbdMax_, kbdStep, kbdCur});
} else {
items_.push_back({ControlId::KbdBright, kIconKeyboard, kIconKeyboard,
"Keypad", false, 0, 1, 1, (kbdCur > 0) ? 1 : 0});
}
}
std::vector<ControlItem>& items() { return items_; }
const std::vector<ControlItem>& items() const { return items_; }
void adjust(size_t idx, int dir) {
if (idx >= items_.size()) {
return;
}
ControlItem& it = items_[idx];
if (it.isSlider) {
it.value = std::clamp(it.value + dir * it.stepVal, it.minVal, it.maxVal);
} else {
it.value = it.value ? 0 : 1;
}
apply(it);
}
void activate(size_t idx) {
if (idx >= items_.size()) {
return;
}
ControlItem& it = items_[idx];
if (!it.isSlider) {
it.value = it.value ? 0 : 1;
apply(it);
}
}
private:
void apply(ControlItem& it) {
switch (it.id) {
case ControlId::SpeakerVol: {
long hw = std::lround((it.value * 84.0) / 100.0);
alsa_.setInteger("RX3 Digital Volume", hw);
char cmd[128];
std::snprintf(
cmd, sizeof(cmd),
"wpctl set-volume @DEFAULT_AUDIO_SINK@ %d%% >/dev/null 2>&1; "
"sxmo_hook_statusbar.sh audio >/dev/null 2>&1",
it.value);
runAsyncCmd(cmd);
break;
}
case ControlId::EarpieceVol: {
long hw = std::lround((it.value * 84.0) / 100.0);
alsa_.setInteger("RX1 Digital Volume", hw);
break;
}
case ControlId::Flashlight: {
int v = it.value ? torchMax_ : 0;
if (!writeIntFile(torchDir_ + "/brightness", v)) {
runAsyncCmd(it.value ? "brightnessctl -d white:torch set 255"
: "brightnessctl -d white:torch set 0");
}
break;
}
case ControlId::SoundMute: {
alsa_.setBoolean("SPK DAC Switch", it.value != 0);
runAsyncCmd(it.value
? "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1; "
"sxmo_hook_statusbar.sh audio >/dev/null 2>&1"
: "wpctl set-mute @DEFAULT_AUDIO_SINK@ 1 >/dev/null 2>&1; "
"sxmo_hook_statusbar.sh audio >/dev/null 2>&1");
break;
}
case ControlId::MicMute: {
alsa_.setEnum("DEC1 MUX", it.value ? savedMicMux_ : 0);
alsa_.setInteger("ADC3 Volume", it.value ? 8 : 0);
runAsyncCmd(it.value
? "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0 >/dev/null 2>&1"
: "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1 >/dev/null 2>&1");
break;
}
case ControlId::ScreenBright: {
writeIntFile(backlightDir_ + "/bl_power", 0);
writeIntFile(backlightDir_ + "/brightness", it.value);
writeIntFile("/tmp/sxmo_saved_brightness", it.value);
break;
}
case ControlId::KbdBright: {
int v = kbdSupportsSlider_ ? it.value : (it.value ? kbdMax_ : 0);
if (!writeIntFile(kbdDir_ + "/brightness", v)) {
char cmd[128];
std::snprintf(cmd, sizeof(cmd),
"brightnessctl -d white:kbd_backlight set %d >/dev/null 2>&1", v);
runAsyncCmd(cmd);
}
break;
}
}
}
AlsaControl alsa_;
std::string torchDir_;
int torchMax_ = 255;
std::string backlightDir_;
int screenMax_ = 14;
std::string kbdDir_;
int kbdMax_ = 1;
bool kbdSupportsSlider_ = false;
unsigned int savedMicMux_ = 2;
std::vector<ControlItem> items_;
};
} // namespace
int main() {
int64_t startMs = nowMs();
std::string pidPath = getRuntimeFile("xdrawer.pid");
std::string startPath = getRuntimeFile("xdrawer.start");
int existingPid = readIntFile(pidPath, -1);
if (existingPid > 0 && existingPid != static_cast<int>(::getpid())) {
if (::kill(existingPid, 0) == 0) {
int fd = ::open(startPath.c_str(), O_RDONLY | O_CLOEXEC);
int64_t prevStartMs = 0;
if (fd >= 0) {
char buf[64];
ssize_t n = ::read(fd, buf, sizeof(buf) - 1);
::close(fd);
if (n > 0) {
buf[n] = '\0';
prevStartMs = std::atoll(buf);
}
}
if (startMs - prevStartMs < 480) {
return 0;
}
::kill(existingPid, SIGTERM);
return 0;
}
}
writeIntFile(pidPath, static_cast<int>(::getpid()));
{
int fd = ::open(startPath.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, 0644);
if (fd >= 0) {
char buf[32];
int len = std::snprintf(buf, sizeof(buf), "%lld\n",
static_cast<long long>(startMs));
(void)::write(fd, buf, len);
::close(fd);
}
}
struct sigaction sa {};
sa.sa_handler = handleSignal;
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
runAsyncCmd("pkill -x dmenu >/dev/null 2>&1");
Display* dpy = XOpenDisplay(nullptr);
if (!dpy) {
::unlink(pidPath.c_str());
return 1;
}
int screen = DefaultScreen(dpy);
Window root = RootWindow(dpy, screen);
Visual* visual = DefaultVisual(dpy, screen);
Colormap cmap = DefaultColormap(dpy, screen);
XSetWindowAttributes swa{};
swa.override_redirect = True;
swa.background_pixel = 0x222222;
swa.border_pixel = 0xf4d03f;
swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask |
StructureNotifyMask | VisibilityChangeMask;
Window win = XCreateWindow(
dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0,
DefaultDepth(dpy, screen), CopyFromParent, visual,
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa);
XClassHint ch{};
char resName[] = "xdrawer";
char resClass[] = "XDrawer";
ch.res_name = resName;
ch.res_class = resClass;
XSetClassHint(dpy, win, &ch);
XStoreName(dpy, win, "XDrawer");
XMapRaised(dpy, win);
XSync(dpy, False);
for (int i = 0; i < 60; ++i) {
if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) ==
GrabSuccess) {
break;
}
usleep(5000);
}
Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, DefaultDepth(dpy, screen));
GC gc = XCreateGC(dpy, pix, 0, nullptr);
XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap);
// Use the exact same Pango+Xft font stack as dwm ("Fira Code, DejaVuSansM Nerd Font 15px")
PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen);
PangoContext* pangoCtx = pango_font_map_create_context(fontMap);
PangoLayout* layoutMain = pango_layout_new(pangoCtx);
PangoFontDescription* descMain =
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 15px");
pango_layout_set_font_description(layoutMain, descMain);
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
PangoFontDescription* descIcon =
pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 16px");
pango_layout_set_font_description(layoutIcon, descIcon);
PangoLayout* layoutSmall = pango_layout_new(pangoCtx);
PangoFontDescription* descSmall =
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 12px");
pango_layout_set_font_description(layoutSmall, descSmall);
auto allocColor = [&](const char* hex) {
XftColor col{};
XftColorAllocName(dpy, visual, cmap, hex, &col);
return col;
};
XftColor colWhite = allocColor("#ffffff");
XftColor colLight = allocColor("#dddddd");
XftColor colMuted = allocColor("#aaaaaa");
XftColor colYellow = allocColor("#f4d03f");
XftColor colGreen = allocColor("#2ecc71");
PhoneController controller;
size_t selected = 0;
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h,
const std::string& text, int maxW = -1) {
pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.size()));
if (maxW > 0) {
pango_layout_set_width(layout, maxW * PANGO_SCALE);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
} else {
pango_layout_set_width(layout, -1);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
}
PangoRectangle ink{}, logical{};
pango_layout_get_pixel_extents(layout, &ink, &logical);
int ty = y + (h - logical.height) / 2;
pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE,
ty * PANGO_SCALE);
};
auto pangoWidth = [&](PangoLayout* layout, const std::string& text) -> int {
pango_layout_set_width(layout, -1);
pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.size()));
PangoRectangle ink{}, logical{};
pango_layout_get_pixel_extents(layout, &ink, &logical);
return logical.width;
};
auto render = [&]() {
XSetForeground(dpy, gc, 0x222222);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
// 7 Control rows (42px each, 7 * 42 = 294px exact)
const auto& items = controller.items();
for (size_t i = 0; i < items.size(); ++i) {
int ry = static_cast<int>(i) * kRowHeight;
bool isSel = (i == selected);
XSetForeground(dpy, gc, isSel ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e));
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, kRowHeight);
if (isSel) {
XSetForeground(dpy, gc, 0xf4d03f);
XFillRectangle(dpy, pix, gc, 0, ry, 4, kRowHeight);
}
const ControlItem& it = items[i];
bool activeState = (it.value > 0);
const char* rowIcon = activeState ? it.iconOn : it.iconOff;
// Icon column (x = 8..28) + Label column (x = 30..112 for sliders, 30..166 for toggles)
int labelMaxW = it.isSlider ? 82 : 136;
drawPango(layoutIcon, isSel ? &colYellow : &colLight, 8, ry, kRowHeight, rowIcon);
drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry, kRowHeight,
it.label, labelMaxW);
if (it.isSlider) {
int pct = 0;
if (it.maxVal > it.minVal) {
if (it.minVal == 1) {
pct = static_cast<int>(std::lround((it.value * 100.0) / it.maxVal));
} else {
pct = static_cast<int>(
std::lround(((it.value - it.minVal) * 100.0) / (it.maxVal - it.minVal)));
}
}
pct = std::clamp(pct, 0, 100);
// Slider track (x = 114, w = 78, h = 12) + thumb + right-aligned pct (x = 196..234)
int barX = 114;
int barW = 78;
int barH = 12;
int barY = ry + (kRowHeight - barH) / 2;
XSetForeground(dpy, gc, 0x161616);
XFillRectangle(dpy, pix, gc, barX, barY, barW, barH);
XSetForeground(dpy, gc, isSel ? 0xdddddd : 0x555555);
XDrawRectangle(dpy, pix, gc, barX, barY, barW - 1, barH - 1);
int fillW = ((barW - 2) * pct) / 100;
if (fillW > 0) {
XSetForeground(dpy, gc, isSel ? 0xf4d03f : 0x3498db);
XFillRectangle(dpy, pix, gc, barX + 1, barY + 1, fillW, barH - 2);
}
// Slider thumb knob
int knobX = barX + std::clamp(fillW - 2, 0, barW - 5);
XSetForeground(dpy, gc, 0xffffff);
XFillRectangle(dpy, pix, gc, knobX, barY - 2, 5, barH + 4);
char pctBuf[16];
std::snprintf(pctBuf, sizeof(pctBuf), "%d%%", pct);
std::string pctStr(pctBuf);
int pw = pangoWidth(layoutSmall, pctStr);
drawPango(layoutSmall, isSel ? &colWhite : &colLight, kWinWidth - pw - 6,
ry, kRowHeight, pctStr);
} else {
// Toggle switch icon + ON/OFF label (right-aligned within x = 160..234)
bool on = (it.value != 0);
std::string stStr = on ? "ON" : "OFF";
int stW = pangoWidth(layoutMain, stStr);
drawPango(layoutMain, on ? (isSel ? &colYellow : &colGreen) : &colMuted,
kWinWidth - stW - 8, ry, kRowHeight, stStr);
drawPango(layoutIcon, on ? (isSel ? &colYellow : &colGreen) : &colMuted,
kWinWidth - stW - 30, ry, kRowHeight,
on ? kIconToggleOn : kIconToggleOff);
}
}
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
XFlush(dpy);
};
render();
auto writeSoftkeySuppress = [](int64_t untilMs) {
int fd = ::open("/tmp/sxmo_softkey_suppress",
O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd >= 0) {
char buf[32];
int len = std::snprintf(buf, sizeof(buf), "%lld\n",
static_cast<long long>(untilMs));
(void)::write(fd, buf, len);
::close(fd);
}
};
auto querySoftkeysDown = [&](Display* d) -> int {
char km[32];
if (!XQueryKeymap(d, km)) {
return 0;
}
int mask = 0;
if (km[135 / 8] & (1 << (135 % 8))) {
mask |= 1;
}
if (km[166 / 8] & (1 << (166 % 8))) {
mask |= 2;
}
return mask;
};
// Drain any stale key events that arrived before initial render finished
while (XPending(dpy)) {
XEvent ev;
XNextEvent(dpy, &ev);
if (ev.type == Expose && ev.xexpose.count == 0) {
render();
}
}
int64_t startupIgnoreUntilMs = std::max(startMs + 220, nowMs() + 60);
bool initialKeysReleased = (querySoftkeysDown(dpy) == 0);
bool closedByKey = false;
int xfd = ConnectionNumber(dpy);
while (gRunning) {
if (!initialKeysReleased && querySoftkeysDown(dpy) == 0) {
initialKeysReleased = true;
}
while (XPending(dpy)) {
XEvent ev;
XNextEvent(dpy, &ev);
if (!initialKeysReleased && querySoftkeysDown(dpy) == 0) {
initialKeysReleased = true;
}
if (ev.type == Expose) {
if (ev.xexpose.count == 0) {
render();
}
} else if (ev.type == VisibilityNotify) {
if (ev.xvisibility.state != VisibilityUnobscured) {
XRaiseWindow(dpy, win);
XFlush(dpy);
}
} else if (ev.type == KeyRelease) {
KeySym sym = XLookupKeysym(&ev.xkey, 0);
if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 ||
sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 ||
ev.xkey.keycode == 135 || ev.xkey.keycode == 166) {
if (querySoftkeysDown(dpy) == 0) {
initialKeysReleased = true;
}
}
} else if (ev.type == KeyPress) {
KeySym sym = XLookupKeysym(&ev.xkey, 0);
size_t count = controller.items().size();
if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 ||
sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 ||
ev.xkey.keycode == 135 || ev.xkey.keycode == 166) {
if (initialKeysReleased && nowMs() >= startupIgnoreUntilMs) {
writeSoftkeySuppress(nowMs() + 500);
closedByKey = true;
gRunning = 0;
break;
}
continue;
}
switch (sym) {
case XK_Escape:
case XK_BackSpace:
case XK_q:
writeSoftkeySuppress(nowMs() + 400);
closedByKey = true;
gRunning = 0;
break;
case XK_Up:
case XK_KP_Up:
case XK_k:
if (count > 0) {
selected = (selected + count - 1) % count;
render();
}
break;
case XK_Down:
case XK_KP_Down:
case XK_j:
if (count > 0) {
selected = (selected + 1) % count;
render();
}
break;
case XK_Left:
case XK_KP_Left:
case XK_h:
case XF86XK_AudioLowerVolume:
controller.adjust(selected, -1);
render();
break;
case XK_Right:
case XK_KP_Right:
case XK_l:
case XF86XK_AudioRaiseVolume:
controller.adjust(selected, +1);
render();
break;
case XK_Return:
case XK_KP_Enter:
case XK_space:
case XF86XK_Phone:
case XF86XK_PickupPhone:
controller.activate(selected);
render();
break;
case XK_1:
case XK_2:
case XK_3:
case XK_4:
case XK_5:
case XK_6:
case XK_7: {
size_t idx = static_cast<size_t>(sym - XK_1);
if (idx < count) {
selected = idx;
controller.activate(selected);
render();
}
break;
}
default:
break;
}
}
}
if (!gRunning) {
break;
}
struct pollfd pfd {};
pfd.fd = xfd;
pfd.events = POLLIN;
::poll(&pfd, 1, initialKeysReleased ? 250 : 40);
}
// Absorb any still-held softkeys (or second softkey of a closing chord)
// before releasing XGrabKeyboard so dwm never sees a trailing softkey press.
if (closedByKey) {
int64_t drainDeadline = nowMs() + 260;
while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) {
while (XPending(dpy)) {
XEvent ev;
XNextEvent(dpy, &ev);
}
struct pollfd pfd {};
pfd.fd = xfd;
pfd.events = POLLIN;
::poll(&pfd, 1, 15);
}
while (XPending(dpy)) {
XEvent ev;
XNextEvent(dpy, &ev);
}
writeSoftkeySuppress(nowMs() + 400);
}
XUngrabKeyboard(dpy, CurrentTime);
pango_font_description_free(descSmall);
pango_font_description_free(descIcon);
pango_font_description_free(descMain);
g_object_unref(layoutSmall);
g_object_unref(layoutIcon);
g_object_unref(layoutMain);
g_object_unref(pangoCtx);
XftDrawDestroy(xftDraw);
XFreeGC(dpy, gc);
XFreePixmap(dpy, pix);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
::unlink(pidPath.c_str());
return 0;
}