feat(dwm): add dynamic plugin system with xdrawer, dialer, and calllog plugins

This commit is contained in:
Luxferre
2026-10-21 11:11:45 +03:00
parent 7b7ae1dcc5
commit 0a02400505
28 changed files with 2589 additions and 22 deletions
+744
View File
@@ -0,0 +1,744 @@
// SPDX-License-Identifier: GPL-2.0-only
// Dialer DWM Plugin - Keypad-driven X11 dialer with contact & T9 integration (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 <pango/pango.h>
#include <pango/pangoxft.h>
#include <algorithm>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <poll.h>
#include <string>
#include <vector>
#include "../sxmo-dwm/dwm_plugin.h"
#include "phone_data.h"
namespace {
using namespace bananarchy;
volatile sig_atomic_t gRunning = 1;
void handleSignal(int) {
gRunning = 0;
}
constexpr const char* kQueuePath = "/tmp/sxmo_dialer_queue";
void enqueueDialChars(const char* chars) {
if (!chars || !chars[0]) {
return;
}
int fd = ::open(kQueuePath, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
if (fd >= 0) {
(void)::write(fd, chars, std::strlen(chars));
::close(fd);
}
}
std::string drainDialQueue() {
int fd = ::open(kQueuePath, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return "";
}
char buf[128];
ssize_t n = ::read(fd, buf, sizeof(buf) - 1);
::close(fd);
::unlink(kQueuePath);
if (n <= 0) {
return "";
}
buf[n] = '\0';
return normalizePhone(buf);
}
int dialer_run_main(int argc, char** argv) {
gRunning = 1;
int64_t startMs = nowMs();
if (argc > 1 && argv[1] && argv[1][0]) {
enqueueDialChars(argv[1]);
}
std::string pidPath = getRuntimeFile("xdialer.pid");
int existingPid = readIntFile(pidPath, -1);
if (existingPid > 0 && existingPid != static_cast<int>(::getpid())) {
if (::kill(existingPid, 0) == 0) {
return 0;
}
}
writeIntFile(pidPath, static_cast<int>(::getpid()));
struct sigaction sa {};
sa.sa_handler = handleSignal;
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
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 = 0x005577;
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[] = "xdialer";
char resClass[] = "XDialer";
ch.res_name = resName;
ch.res_class = resClass;
XSetClassHint(dpy, win, &ch);
XStoreName(dpy, win, "Dialer");
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);
PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen);
PangoContext* pangoCtx = pango_font_map_create_context(fontMap);
PangoLayout* layoutNum = pango_layout_new(pangoCtx);
PangoFontDescription* descNum =
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font Bold 18px");
pango_layout_set_font_description(layoutNum, descNum);
PangoLayout* layoutMain = pango_layout_new(pangoCtx);
PangoFontDescription* descMain =
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 14px");
pango_layout_set_font_description(layoutMain, descMain);
PangoLayout* layoutSmall = pango_layout_new(pangoCtx);
PangoFontDescription* descSmall =
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 11px");
pango_layout_set_font_description(layoutSmall, descSmall);
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
PangoFontDescription* descIcon =
pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 15px");
pango_layout_set_font_description(layoutIcon, descIcon);
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("#999999");
XftColor colYellow = allocColor("#f4d03f");
XftColor colGreen = allocColor("#2ecc71");
XftColor colBlue = allocColor("#5dade2");
ContactBook book;
book.load();
std::vector<ContactEntry> allContacts = book.buildDialerList();
std::vector<ContactEntry> filtered;
std::string number = drainDialQueue();
int selected = -1; // -1 = top number box focused; 0..N-1 = contact row
// If launched with '0' from idle desktop and '0' (keycode 19) is still held,
// allow long-pressing that initial '0' to turn into '+'.
bool zeroHeld = (number == "0" && isKeycodeDown(dpy, 19));
int64_t zeroPressMs = zeroHeld ? startMs : 0;
auto updateFilter = [&]() {
filtered.clear();
if (number.empty()) {
filtered = allContacts;
if (selected >= static_cast<int>(filtered.size())) {
selected = filtered.empty() ? -1 : 0;
}
if (selected < 0 && !filtered.empty()) {
selected = 0;
}
return;
}
std::string qNorm = normalizePhone(number);
std::string qLower = toLowerStr(number);
std::vector<ContactEntry> prefixMatches;
std::vector<ContactEntry> subMatches;
for (const auto& c : allContacts) {
bool pfx = (c.normNumber.rfind(qNorm, 0) == 0) ||
(!qNorm.empty() && c.t9Name.rfind(qNorm, 0) == 0);
bool sub = pfx ||
(c.normNumber.find(qNorm) != std::string::npos) ||
(!qNorm.empty() && c.t9Name.find(qNorm) != std::string::npos) ||
(c.lowerName.find(qLower) != std::string::npos);
if (pfx) {
prefixMatches.push_back(c);
} else if (sub) {
subMatches.push_back(c);
}
}
filtered.insert(filtered.end(), prefixMatches.begin(), prefixMatches.end());
filtered.insert(filtered.end(), subMatches.begin(), subMatches.end());
if (selected >= static_cast<int>(filtered.size())) {
selected = -1;
}
};
updateFilter();
if (!number.empty()) {
selected = -1;
}
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;
};
constexpr int kHeaderH = 56;
constexpr int kSoftbarH = 26;
constexpr int kListY = kHeaderH;
constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH; // 214px
constexpr int kVisibleRows = 5;
auto render = [&]() {
XSetForeground(dpy, gc, 0x222222);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
// 1. Top Number Display Box (y = 0..56)
bool topSel = (selected < 0);
XSetForeground(dpy, gc, topSel ? 0x1b3b4b : 0x1c1c1c);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
if (topSel) {
XSetForeground(dpy, gc, 0xf4d03f);
XFillRectangle(dpy, pix, gc, 0, 0, 4, kHeaderH);
}
XSetForeground(dpy, gc, topSel ? 0x005577 : 0x444444);
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 2, kWinWidth, 2);
drawPango(layoutIcon, &colGreen, 10, 4, 30, kIconPhone);
if (number.empty()) {
drawPango(layoutMain, &colMuted, 32, 4, 30, "Enter number...", 196);
} else {
std::string dispNum = number;
if (dispNum.size() > 13) {
dispNum = ".." + dispNum.substr(dispNum.size() - 12);
}
drawPango(layoutNum, &colWhite, 32, 2, 32, dispNum, 182);
drawPango(layoutIcon, &colMuted, 218, 4, 30, kIconBackspace);
}
// Subtitle inside header box (y = 33..54)
std::string subText;
XftColor* subCol = &colMuted;
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
const auto& c = filtered[selected];
subText = "Dial: " + c.number;
subCol = &colYellow;
} else if (!number.empty()) {
std::string exactName = book.resolveName(number);
if (!exactName.empty()) {
subText = exactName;
subCol = &colGreen;
} else if (!filtered.empty()) {
subText = filtered[0].name + " (" + filtered[0].number + ")";
subCol = &colBlue;
} else {
subText = "Press Call to dial (Hold 0: +)";
}
} else {
subText = "0-9/*/# Dial | Hold 0: +";
}
drawPango(layoutSmall, subCol, 32, 33, 20, subText, 202);
// 2. Contact / Recent Suggestions List (y = 56..270, 5 visible rows)
if (filtered.empty()) {
drawPango(layoutMain, &colMuted, 16, kListY + 40, 30,
number.empty() ? "No saved contacts" : "New number (press Call)",
208);
} else {
int total = static_cast<int>(filtered.size());
int focusIdx = (selected >= 0) ? selected : 0;
int scrollOffset = 0;
if (focusIdx >= kVisibleRows) {
scrollOffset = focusIdx - kVisibleRows + 1;
}
if (scrollOffset > std::max(0, total - kVisibleRows)) {
scrollOffset = std::max(0, total - kVisibleRows);
}
for (int i = 0; i < kVisibleRows; ++i) {
int idx = scrollOffset + i;
if (idx >= total) {
break;
}
int ry = kListY + (i * kListH) / kVisibleRows;
int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows;
int rowH = nextRy - ry;
bool isSel = (idx == selected);
XSetForeground(dpy, gc,
isSel ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e));
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH);
if (isSel) {
XSetForeground(dpy, gc, 0xf4d03f);
XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH);
}
const ContactEntry& c = filtered[idx];
const char* icon = c.isRecent ? kIconClock : kIconUser;
XftColor* icCol = isSel ? &colYellow : (c.isRecent ? &colGreen : &colBlue);
drawPango(layoutIcon, icCol, 8, ry, rowH, icon);
if (c.name != c.number) {
drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 2,
22, c.name, 200);
drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 22,
18, c.number, 200);
} else {
drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 2,
22, c.number, 200);
drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 22,
18, "Recent number", 200);
}
}
if (total > kVisibleRows) {
int sbH = std::max(16, (kListH * kVisibleRows) / total);
int sbY = kListY +
((kListH - sbH) * scrollOffset) / (total - kVisibleRows);
XSetForeground(dpy, gc, 0x666666);
XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH);
}
}
// 3. Bottom Softkey Bar (y = 270..296)
int sbY = kWinHeight - kSoftbarH;
XSetForeground(dpy, gc, 0x181818);
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH);
XSetForeground(dpy, gc, 0x444444);
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1);
std::string leftLabel = number.empty() ? "Call Log" : "+Sign";
std::string centerLabel = "CALL";
std::string rightLabel = number.empty() ? "Close" : "Clear";
drawPango(layoutSmall, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel);
int cw = pangoWidth(layoutMain, centerLabel);
drawPango(layoutMain, &colGreen, (kWinWidth - cw) / 2, sbY + 1,
kSoftbarH - 1, centerLabel);
int rw = pangoWidth(layoutSmall, rightLabel);
drawPango(layoutSmall, &colLight, kWinWidth - rw - 6, sbY + 1,
kSoftbarH - 1, rightLabel);
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
XFlush(dpy);
};
render();
int64_t startupIgnoreUntilMs = startMs + 90;
bool initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
int softkeyPending = 0;
int64_t softkeyDeadlineMs = 0;
bool closedByKey = false;
std::string targetToDial;
bool switchToCallLog = false;
bool launchXDrawer = false;
int xfd = ConnectionNumber(dpy);
auto selectActiveNumber = [&]() -> std::string {
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
return filtered[selected].number;
}
if (!number.empty()) {
return number;
}
if (!filtered.empty()) {
return filtered[0].number;
}
return "";
};
while (gRunning) {
std::string extra = drainDialQueue();
if (!extra.empty()) {
number += extra;
selected = -1;
updateFilter();
render();
}
if (zeroHeld) {
if (!isKeycodeDown(dpy, 19)) {
zeroHeld = false;
} else if (nowMs() - zeroPressMs >= 420) {
zeroHeld = false;
if (!number.empty() && number.back() == '0') {
number.back() = '+';
updateFilter();
render();
}
}
}
if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
initialSoftkeysReleased = true;
}
while (XPending(dpy)) {
XEvent ev;
XNextEvent(dpy, &ev);
if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
initialSoftkeysReleased = 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 (ev.xkey.keycode == 19 || sym == XK_0) {
zeroHeld = false;
}
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) {
initialSoftkeysReleased = true;
}
}
} else if (ev.type == KeyPress) {
KeySym sym = XLookupKeysym(&ev.xkey, 0);
bool isLeftSoft = (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 ||
ev.xkey.keycode == 135);
bool isRightSoft = (sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 ||
ev.xkey.keycode == 166);
if (isLeftSoft || isRightSoft) {
if (!initialSoftkeysReleased || nowMs() < startupIgnoreUntilMs) {
continue;
}
int which = (isLeftSoft ? 1 : 0) | (isRightSoft ? 2 : 0);
softkeyPending |= which | querySoftkeysDown(dpy);
if (softkeyPending == 3) {
writeSoftkeySuppress(nowMs() + 500);
launchXDrawer = true;
closedByKey = true;
gRunning = 0;
break;
}
if (softkeyDeadlineMs == 0) {
softkeyDeadlineMs = nowMs() + 45;
}
continue;
}
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
if (dialCh != '\0') {
if (nowMs() - startMs < 45 && number.size() == 1 &&
number[0] == dialCh) {
// Ignore duplicate event from the initial key press that opened dialer
continue;
}
number.push_back(dialCh);
selected = -1;
if (dialCh == '0') {
zeroHeld = true;
zeroPressMs = nowMs();
} else {
zeroHeld = false;
}
updateFilter();
render();
continue;
}
switch (sym) {
case XK_Escape:
writeSoftkeySuppress(nowMs() + 350);
closedByKey = true;
gRunning = 0;
break;
case XK_BackSpace:
case XK_Left:
case XK_KP_Left:
if (!number.empty()) {
number.pop_back();
selected = -1;
updateFilter();
render();
} else if (sym == XK_BackSpace) {
closedByKey = true;
gRunning = 0;
}
break;
case XK_Right:
case XK_KP_Right:
if (number.empty() || number[0] != '+') {
number = "+" + number;
selected = -1;
updateFilter();
render();
}
break;
case XK_Up:
case XK_KP_Up:
if (!filtered.empty()) {
if (selected <= -1) {
selected = static_cast<int>(filtered.size()) - 1;
} else if (selected == 0 && !number.empty()) {
selected = -1;
} else if (selected == 0) {
selected = static_cast<int>(filtered.size()) - 1;
} else {
--selected;
}
render();
}
break;
case XK_Down:
case XK_KP_Down:
if (!filtered.empty()) {
if (selected < 0) {
selected = 0;
} else if (selected + 1 < static_cast<int>(filtered.size())) {
++selected;
} else {
selected = number.empty() ? 0 : -1;
}
render();
}
break;
case XK_Return:
case XK_KP_Enter:
case XF86XK_Phone:
case XF86XK_PickupPhone: {
std::string target = selectActiveNumber();
if (!target.empty()) {
targetToDial = target;
writeSoftkeySuppress(nowMs() + 400);
closedByKey = true;
gRunning = 0;
}
break;
}
default:
break;
}
}
}
if (!gRunning) {
break;
}
if (softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) {
int comb = softkeyPending | querySoftkeysDown(dpy);
softkeyPending = 0;
softkeyDeadlineMs = 0;
if (comb == 3) {
writeSoftkeySuppress(nowMs() + 500);
launchXDrawer = true;
closedByKey = true;
gRunning = 0;
break;
} else if (comb == 1) {
// Left Softkey: if empty, switch to Call Log; if non-empty, toggle leading '+'
if (number.empty()) {
writeSoftkeySuppress(nowMs() + 350);
switchToCallLog = true;
closedByKey = true;
gRunning = 0;
break;
} else {
if (number[0] == '+') {
number.erase(0, 1);
} else {
number = "+" + number;
}
selected = -1;
updateFilter();
render();
}
} else if (comb == 2) {
// Right Softkey: delete last character if non-empty, else close Dialer
if (!number.empty()) {
number.pop_back();
selected = -1;
updateFilter();
render();
} else {
writeSoftkeySuppress(nowMs() + 400);
closedByKey = true;
gRunning = 0;
break;
}
}
}
int timeoutMs = 200;
if (softkeyDeadlineMs > 0) {
timeoutMs = static_cast<int>(std::max<int64_t>(5, softkeyDeadlineMs - nowMs()));
} else if (zeroHeld) {
timeoutMs = 25;
}
struct pollfd pfd {};
pfd.fd = xfd;
pfd.events = POLLIN;
::poll(&pfd, 1, timeoutMs);
}
if (closedByKey && !launchXDrawer) {
int64_t drainDeadline = nowMs() + 200;
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);
}
writeSoftkeySuppress(nowMs() + 350);
}
XUngrabKeyboard(dpy, CurrentTime);
pango_font_description_free(descIcon);
pango_font_description_free(descSmall);
pango_font_description_free(descMain);
pango_font_description_free(descNum);
g_object_unref(layoutIcon);
g_object_unref(layoutSmall);
g_object_unref(layoutMain);
g_object_unref(layoutNum);
g_object_unref(pangoCtx);
XftDrawDestroy(xftDraw);
XFreeGC(dpy, gc);
XFreePixmap(dpy, pix);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
::unlink(pidPath.c_str());
if (launchXDrawer) {
runAsyncArgv("xdrawer");
} else if (switchToCallLog) {
runAsyncArgv("xcalllog");
} else if (!targetToDial.empty()) {
placePhoneCall(targetToDial);
}
return 0;
}
int64_t gLastSpawnMs = 0;
int dialer_plugin_init(const DwmHostApi*) {
return 0;
}
int dialer_on_idle_key(const DwmHostApi* host, KeySym keysym,
unsigned int keycode, unsigned int state) {
if (!host || !host->is_idle_desktop || !host->is_idle_desktop()) {
return 0;
}
char ch = mapDialKey(keysym, keycode, state);
if (ch == '\0') {
return 0;
}
char buf[2] = {ch, '\0'};
enqueueDialChars(buf);
int64_t now = nowMs();
std::string pidPath = getRuntimeFile("xdialer.pid");
int existingPid = readIntFile(pidPath, -1);
bool alreadyRunning = (existingPid > 0 && ::kill(existingPid, 0) == 0);
if (!alreadyRunning && (now - gLastSpawnMs) > 250 && host->spawn_plugin) {
gLastSpawnMs = now;
host->spawn_plugin("dialer", nullptr);
}
return 1;
}
} // namespace
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
DWM_PLUGIN_API_VERSION,
"dialer",
"Keypad-driven dialer with contact & T9 integration (opens on idle 0-9/*/#)",
dialer_plugin_init,
nullptr,
nullptr,
dialer_on_idle_key,
nullptr,
dialer_run_main
};