feat(x11): add XDrawer global phone controls drawer, softkey chord detection, and st/T9 scrollback
This commit is contained in:
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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, { } },
|
||||
|
||||
@@ -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, { } },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user