diff --git a/MANUAL.md b/MANUAL.md index c6e10a1..f641604 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -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` | diff --git a/README.md b/README.md index 67c40b7..f8a5a6b 100644 --- a/README.md +++ b/README.md @@ -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]`). diff --git a/overlay/rootfs/usr/bin/XDrawer b/overlay/rootfs/usr/bin/XDrawer new file mode 120000 index 0000000..8199a5b --- /dev/null +++ b/overlay/rootfs/usr/bin/XDrawer @@ -0,0 +1 @@ +xdrawer \ No newline at end of file diff --git a/overlay/rootfs/usr/bin/dmenu b/overlay/rootfs/usr/bin/dmenu index 53d9ec3..aedc8f1 100755 Binary files a/overlay/rootfs/usr/bin/dmenu and b/overlay/rootfs/usr/bin/dmenu differ diff --git a/overlay/rootfs/usr/bin/dwm b/overlay/rootfs/usr/bin/dwm index 920a79f..41234a3 100755 Binary files a/overlay/rootfs/usr/bin/dwm and b/overlay/rootfs/usr/bin/dwm differ diff --git a/overlay/rootfs/usr/bin/st b/overlay/rootfs/usr/bin/st index 2e57535..dd0646d 100755 Binary files a/overlay/rootfs/usr/bin/st and b/overlay/rootfs/usr/bin/st differ diff --git a/overlay/rootfs/usr/bin/xdrawer b/overlay/rootfs/usr/bin/xdrawer new file mode 100755 index 0000000..0e7987e Binary files /dev/null and b/overlay/rootfs/usr/bin/xdrawer differ diff --git a/overlay/rootfs/usr/lib/fcitx5/libt9.so b/overlay/rootfs/usr/lib/fcitx5/libt9.so index 43a7812..098fe27 100755 Binary files a/overlay/rootfs/usr/lib/fcitx5/libt9.so and b/overlay/rootfs/usr/lib/fcitx5/libt9.so differ diff --git a/overlay/rootfs/usr/lib/fcitx5/t9.so b/overlay/rootfs/usr/lib/fcitx5/t9.so index 43a7812..098fe27 100755 Binary files a/overlay/rootfs/usr/lib/fcitx5/t9.so and b/overlay/rootfs/usr/lib/fcitx5/t9.so differ diff --git a/prebuilt/fcitx5-t9/usr/lib/fcitx5/libt9.so b/prebuilt/fcitx5-t9/usr/lib/fcitx5/libt9.so index 43a7812..098fe27 100755 Binary files a/prebuilt/fcitx5-t9/usr/lib/fcitx5/libt9.so and b/prebuilt/fcitx5-t9/usr/lib/fcitx5/libt9.so differ diff --git a/prebuilt/fcitx5-t9/usr/lib/fcitx5/t9.so b/prebuilt/fcitx5-t9/usr/lib/fcitx5/t9.so index 43a7812..098fe27 100755 Binary files a/prebuilt/fcitx5-t9/usr/lib/fcitx5/t9.so and b/prebuilt/fcitx5-t9/usr/lib/fcitx5/t9.so differ diff --git a/prebuilt/sxmo-x11/usr/bin/XDrawer b/prebuilt/sxmo-x11/usr/bin/XDrawer new file mode 120000 index 0000000..8199a5b --- /dev/null +++ b/prebuilt/sxmo-x11/usr/bin/XDrawer @@ -0,0 +1 @@ +xdrawer \ No newline at end of file diff --git a/prebuilt/sxmo-x11/usr/bin/dmenu b/prebuilt/sxmo-x11/usr/bin/dmenu index 53d9ec3..aedc8f1 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dmenu and b/prebuilt/sxmo-x11/usr/bin/dmenu differ diff --git a/prebuilt/sxmo-x11/usr/bin/dwm b/prebuilt/sxmo-x11/usr/bin/dwm index 920a79f..41234a3 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/dwm and b/prebuilt/sxmo-x11/usr/bin/dwm differ diff --git a/prebuilt/sxmo-x11/usr/bin/st b/prebuilt/sxmo-x11/usr/bin/st index 2e57535..dd0646d 100755 Binary files a/prebuilt/sxmo-x11/usr/bin/st and b/prebuilt/sxmo-x11/usr/bin/st differ diff --git a/prebuilt/sxmo-x11/usr/bin/xdrawer b/prebuilt/sxmo-x11/usr/bin/xdrawer new file mode 100755 index 0000000..0e7987e Binary files /dev/null and b/prebuilt/sxmo-x11/usr/bin/xdrawer differ diff --git a/scripts/build-sxmo-x11.sh b/scripts/build-sxmo-x11.sh index e3fc4d9..2de30bb 100755 --- a/scripts/build-sxmo-x11.sh +++ b/scripts/build-sxmo-x11.sh @@ -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 ' diff --git a/src/fcitx5-t9/src/t9.cpp b/src/fcitx5-t9/src/t9.cpp index d8342bb..7f4456e 100644 --- a/src/fcitx5-t9/src/t9.cpp +++ b/src/fcitx5-t9/src/t9.cpp @@ -421,6 +421,8 @@ std::vector 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& 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& items, int index) { preedit += " "; } } - if (total > kPageSize) { + if (start > 0 || end < total) { preedit += (end < total) ? ">" : "<"; } if (preedit.size() < kFixedPreeditChars) { diff --git a/src/sxmo-dmenu/dmenu.c b/src/sxmo-dmenu/dmenu.c index 788f48c..24cb193 100644 --- a/src/sxmo-dmenu/dmenu.c +++ b/src/sxmo-dmenu/dmenu.c @@ -4,6 +4,7 @@ #define _EVDEVK(_v) (0x10081000 + (_v)) #endif #include +#include #include #include #include @@ -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 + 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); } } } diff --git a/src/sxmo-dwm/dwm.c b/src/sxmo-dwm/dwm.c index eac2eb0..98b6f14 100644 --- a/src/sxmo-dwm/dwm.c +++ b/src/sxmo-dwm/dwm.c @@ -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); diff --git a/src/sxmo-st/config.def.h b/src/sxmo-st/config.def.h index 5cfc1e6..9914b9d 100644 --- a/src/sxmo-st/config.def.h +++ b/src/sxmo-st/config.def.h @@ -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, { } }, diff --git a/src/sxmo-st/config.h b/src/sxmo-st/config.h index 5cfc1e6..9914b9d 100644 --- a/src/sxmo-st/config.h +++ b/src/sxmo-st/config.h @@ -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, { } }, diff --git a/src/xdrawer/xdrawer.cpp b/src/xdrawer/xdrawer.cpp new file mode 100644 index 0000000..9c732da --- /dev/null +++ b/src/xdrawer/xdrawer.cpp @@ -0,0 +1,871 @@ +// SPDX-License-Identifier: GPL-2.0-only +// XDrawer - Lightweight X11 global phone controls drawer for Bananarchy (240x320) + +#include +#include +#include +#include +#include +#include +#ifndef _EVDEVK +#define _EVDEVK(_v) (0x10081000 + (_v)) +#endif +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(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(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(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(savedMicMux_)); + } else { + savedMicMux_ = static_cast(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& items() { return items_; } + const std::vector& 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 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(::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(::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(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(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(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(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(std::lround((it.value * 100.0) / it.maxVal)); + } else { + pct = static_cast( + 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(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(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; +}