2026-10-21 11:11:45 +03:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-only
|
2026-10-21 11:57:23 +03:00
|
|
|
// Dialer DWM Plugin - Preloaded resident X11 dialer with contact & T9 integration (240x320)
|
2026-10-21 11:11:45 +03:00
|
|
|
|
|
|
|
|
#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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int dialer_run_main(int argc, char** argv) {
|
|
|
|
|
gRunning = 1;
|
2026-10-21 11:57:23 +03:00
|
|
|
bool isPreload = false;
|
|
|
|
|
std::string initArg;
|
2026-10-21 11:11:45 +03:00
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
|
|
|
if (argv[i] && std::strcmp(argv[i], "--preload") == 0) {
|
|
|
|
|
isPreload = true;
|
|
|
|
|
} else if (argv[i] && argv[i][0]) {
|
|
|
|
|
initArg += argv[i];
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-21 11:57:23 +03:00
|
|
|
|
|
|
|
|
// If the resident dialer daemon is already running, forward the command via FIFO
|
|
|
|
|
if (sendPluginCommand("dialer", isPreload ? "NOP" : (initArg.empty() ? "OPEN" : ("OPEN " + initArg)))) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string daemonPidPath = getPluginDaemonPidPath("dialer");
|
|
|
|
|
std::string activePidPath = getRuntimeFile("xdialer.pid");
|
|
|
|
|
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
|
|
|
|
int fifoFd = setupPluginDaemonFifo("dialer");
|
2026-10-21 11:11:45 +03:00
|
|
|
|
|
|
|
|
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) {
|
2026-10-21 11:57:23 +03:00
|
|
|
if (fifoFd >= 0) ::close(fifoFd);
|
|
|
|
|
::unlink(daemonPidPath.c_str());
|
2026-10-21 11:11:45 +03:00
|
|
|
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");
|
|
|
|
|
|
|
|
|
|
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 =
|
2026-10-21 12:32:21 +03:00
|
|
|
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 15px");
|
2026-10-21 11:11:45 +03:00
|
|
|
pango_layout_set_font_description(layoutMain, descMain);
|
|
|
|
|
|
|
|
|
|
PangoLayout* layoutSmall = pango_layout_new(pangoCtx);
|
|
|
|
|
PangoFontDescription* descSmall =
|
2026-10-21 12:32:21 +03:00
|
|
|
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 12px");
|
2026-10-21 11:11:45 +03:00
|
|
|
pango_layout_set_font_description(layoutSmall, descSmall);
|
|
|
|
|
|
|
|
|
|
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
|
|
|
|
|
PangoFontDescription* descIcon =
|
2026-10-21 12:32:21 +03:00
|
|
|
pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 16px");
|
2026-10-21 11:11:45 +03:00
|
|
|
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;
|
2026-10-21 11:57:23 +03:00
|
|
|
std::vector<ContactEntry> allContacts;
|
2026-10-21 11:11:45 +03:00
|
|
|
std::vector<ContactEntry> filtered;
|
2026-10-21 11:57:23 +03:00
|
|
|
FileStamp contactsStamp;
|
|
|
|
|
FileStamp logStamp;
|
|
|
|
|
bool contactsLoadedOnce = false;
|
2026-10-21 11:11:45 +03:00
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
auto reloadContactsIfNeeded = [&]() -> bool {
|
|
|
|
|
bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath());
|
|
|
|
|
bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath());
|
|
|
|
|
if (cChanged || lChanged || !contactsLoadedOnce) {
|
|
|
|
|
contactsLoadedOnce = true;
|
|
|
|
|
book.load();
|
|
|
|
|
allContacts = book.buildDialerList();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
2026-10-21 11:11:45 +03:00
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
reloadContactsIfNeeded();
|
|
|
|
|
|
|
|
|
|
std::string number;
|
|
|
|
|
int selected = -1;
|
|
|
|
|
bool visible = false;
|
|
|
|
|
int64_t openStartMs = 0;
|
|
|
|
|
int64_t startupIgnoreUntilMs = 0;
|
|
|
|
|
bool initialSoftkeysReleased = true;
|
|
|
|
|
int softkeyPending = 0;
|
|
|
|
|
int64_t softkeyDeadlineMs = 0;
|
|
|
|
|
bool zeroHeld = false;
|
|
|
|
|
int64_t zeroPressMs = 0;
|
2026-10-21 11:11:45 +03:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
2026-10-21 11:57:23 +03:00
|
|
|
constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH;
|
2026-10-21 11:11:45 +03:00
|
|
|
constexpr int kVisibleRows = 5;
|
|
|
|
|
|
|
|
|
|
auto render = [&]() {
|
|
|
|
|
XSetForeground(dpy, gc, 0x222222);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
2026-10-21 12:32:21 +03:00
|
|
|
drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel);
|
2026-10-21 11:11:45 +03:00
|
|
|
int cw = pangoWidth(layoutMain, centerLabel);
|
|
|
|
|
drawPango(layoutMain, &colGreen, (kWinWidth - cw) / 2, sbY + 1,
|
|
|
|
|
kSoftbarH - 1, centerLabel);
|
2026-10-21 12:32:21 +03:00
|
|
|
int rw = pangoWidth(layoutMain, rightLabel);
|
|
|
|
|
drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1,
|
2026-10-21 11:11:45 +03:00
|
|
|
kSoftbarH - 1, rightLabel);
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
if (visible) {
|
|
|
|
|
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
|
|
|
|
XFlush(dpy);
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
};
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
// Pre-warm Pango/FreeType/Xrender glyph caches into offscreen Pixmap at startup
|
|
|
|
|
number = "+0123456789*#";
|
|
|
|
|
updateFilter();
|
2026-10-21 11:11:45 +03:00
|
|
|
render();
|
2026-10-21 11:57:23 +03:00
|
|
|
drawPango(layoutIcon, &colGreen, 0, 0, 20,
|
|
|
|
|
std::string(kIconPhone) + kIconBackspace + kIconClock + kIconUser);
|
|
|
|
|
number.clear();
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
XSync(dpy, False);
|
|
|
|
|
|
|
|
|
|
auto showWindow = [&](const std::string& arg) {
|
|
|
|
|
if (isPidFileAlive(getRuntimeFile("xcalllog.pid"))) {
|
|
|
|
|
sendPluginCommand("calllog", "CLOSE");
|
|
|
|
|
}
|
2026-10-21 12:32:21 +03:00
|
|
|
if (isPidFileAlive(getRuntimeFile("xcontacts.pid"))) {
|
|
|
|
|
sendPluginCommand("contacts", "CLOSE");
|
|
|
|
|
}
|
2026-10-21 11:57:23 +03:00
|
|
|
if (isPidFileAlive(getRuntimeFile("xdrawer.pid"))) {
|
|
|
|
|
sendPluginCommand("xdrawer", "CLOSE");
|
|
|
|
|
}
|
|
|
|
|
std::string cleanArg = normalizePhone(arg);
|
|
|
|
|
if (!visible) {
|
|
|
|
|
openStartMs = nowMs();
|
|
|
|
|
startupIgnoreUntilMs = openStartMs + 70;
|
|
|
|
|
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
|
|
|
|
softkeyPending = 0;
|
|
|
|
|
softkeyDeadlineMs = 0;
|
|
|
|
|
zeroHeld = (cleanArg == "0" && isKeycodeDown(dpy, 19));
|
|
|
|
|
zeroPressMs = zeroHeld ? openStartMs : 0;
|
|
|
|
|
visible = true;
|
|
|
|
|
XMapRaised(dpy, win);
|
|
|
|
|
if (cleanArg.empty()) {
|
|
|
|
|
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < 20; ++i) {
|
|
|
|
|
if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) ==
|
|
|
|
|
GrabSuccess) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
usleep(2000);
|
|
|
|
|
}
|
|
|
|
|
bool reloaded = reloadContactsIfNeeded();
|
|
|
|
|
if (!cleanArg.empty() || reloaded || number != cleanArg) {
|
|
|
|
|
number = cleanArg;
|
|
|
|
|
selected = number.empty() ? (allContacts.empty() ? -1 : 0) : -1;
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
XFlush(dpy);
|
|
|
|
|
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
|
|
|
|
} else if (!cleanArg.empty()) {
|
|
|
|
|
if (nowMs() - openStartMs < 40 && number == cleanArg) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
number += cleanArg;
|
|
|
|
|
selected = -1;
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto hideWindow = [&](bool drainSoftkeys) {
|
|
|
|
|
if (!visible) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
int xfd = ConnectionNumber(dpy);
|
|
|
|
|
if (drainSoftkeys) {
|
|
|
|
|
int64_t drainDeadline = nowMs() + 180;
|
|
|
|
|
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, 10);
|
|
|
|
|
}
|
|
|
|
|
writeSoftkeySuppress(nowMs() + 300);
|
|
|
|
|
}
|
|
|
|
|
XUngrabKeyboard(dpy, CurrentTime);
|
|
|
|
|
XUnmapWindow(dpy, win);
|
|
|
|
|
XFlush(dpy);
|
|
|
|
|
::unlink(activePidPath.c_str());
|
|
|
|
|
visible = false;
|
|
|
|
|
softkeyPending = 0;
|
|
|
|
|
softkeyDeadlineMs = 0;
|
|
|
|
|
zeroHeld = false;
|
|
|
|
|
if (!number.empty() || selected != (allContacts.empty() ? -1 : 0)) {
|
|
|
|
|
number.clear();
|
|
|
|
|
selected = allContacts.empty() ? -1 : 0;
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
XFlush(dpy);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!isPreload) {
|
|
|
|
|
showWindow(initArg);
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
|
|
|
|
|
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) {
|
2026-10-21 11:57:23 +03:00
|
|
|
// 1. Process any incoming FIFO commands
|
|
|
|
|
for (const std::string& cmd : readFifoLines(fifoFd)) {
|
|
|
|
|
if (cmd == "NOP") {
|
|
|
|
|
continue;
|
|
|
|
|
} else if (cmd == "CLOSE") {
|
|
|
|
|
hideWindow(false);
|
|
|
|
|
} else if (cmd == "OPEN") {
|
|
|
|
|
showWindow("");
|
|
|
|
|
} else if (cmd.rfind("OPEN ", 0) == 0) {
|
|
|
|
|
showWindow(cmd.substr(5));
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
if (visible && zeroHeld) {
|
2026-10-21 11:11:45 +03:00
|
|
|
if (!isKeycodeDown(dpy, 19)) {
|
|
|
|
|
zeroHeld = false;
|
|
|
|
|
} else if (nowMs() - zeroPressMs >= 420) {
|
|
|
|
|
zeroHeld = false;
|
|
|
|
|
if (!number.empty() && number.back() == '0') {
|
|
|
|
|
number.back() = '+';
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
if (visible && !initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
|
2026-10-21 11:11:45 +03:00
|
|
|
initialSoftkeysReleased = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (XPending(dpy)) {
|
|
|
|
|
XEvent ev;
|
|
|
|
|
XNextEvent(dpy, &ev);
|
2026-10-21 11:57:23 +03:00
|
|
|
if (!visible) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
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);
|
2026-10-21 11:57:23 +03:00
|
|
|
hideWindow(false);
|
|
|
|
|
if (!sendPluginCommand("xdrawer", "TOGGLE")) {
|
|
|
|
|
runAsyncArgv("xdrawer");
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (softkeyDeadlineMs == 0) {
|
2026-10-21 11:57:23 +03:00
|
|
|
softkeyDeadlineMs = nowMs() + 40;
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
|
|
|
|
|
if (dialCh != '\0') {
|
2026-10-21 11:57:23 +03:00
|
|
|
if (nowMs() - openStartMs < 40 && number.size() == 1 &&
|
2026-10-21 11:11:45 +03:00
|
|
|
number[0] == dialCh) {
|
|
|
|
|
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:
|
2026-10-21 11:57:23 +03:00
|
|
|
writeSoftkeySuppress(nowMs() + 300);
|
|
|
|
|
hideWindow(true);
|
2026-10-21 11:11:45 +03:00
|
|
|
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) {
|
2026-10-21 11:57:23 +03:00
|
|
|
hideWindow(true);
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
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()) {
|
2026-10-21 11:57:23 +03:00
|
|
|
writeSoftkeySuppress(nowMs() + 350);
|
|
|
|
|
hideWindow(true);
|
|
|
|
|
placePhoneCall(target);
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
if (visible && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) {
|
2026-10-21 11:11:45 +03:00
|
|
|
int comb = softkeyPending | querySoftkeysDown(dpy);
|
|
|
|
|
softkeyPending = 0;
|
|
|
|
|
softkeyDeadlineMs = 0;
|
|
|
|
|
if (comb == 3) {
|
|
|
|
|
writeSoftkeySuppress(nowMs() + 500);
|
2026-10-21 11:57:23 +03:00
|
|
|
hideWindow(false);
|
|
|
|
|
if (!sendPluginCommand("xdrawer", "TOGGLE")) {
|
|
|
|
|
runAsyncArgv("xdrawer");
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
} else if (comb == 1) {
|
|
|
|
|
if (number.empty()) {
|
2026-10-21 11:57:23 +03:00
|
|
|
writeSoftkeySuppress(nowMs() + 250);
|
|
|
|
|
hideWindow(false);
|
|
|
|
|
if (!sendPluginCommand("calllog", "OPEN")) {
|
|
|
|
|
runAsyncArgv("xcalllog");
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
} else {
|
|
|
|
|
if (number[0] == '+') {
|
|
|
|
|
number.erase(0, 1);
|
|
|
|
|
} else {
|
|
|
|
|
number = "+" + number;
|
|
|
|
|
}
|
|
|
|
|
selected = -1;
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
} else if (comb == 2) {
|
|
|
|
|
if (!number.empty()) {
|
|
|
|
|
number.pop_back();
|
|
|
|
|
selected = -1;
|
|
|
|
|
updateFilter();
|
|
|
|
|
render();
|
|
|
|
|
} else {
|
2026-10-21 11:57:23 +03:00
|
|
|
writeSoftkeySuppress(nowMs() + 350);
|
|
|
|
|
hideWindow(true);
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
int timeoutMs = -1;
|
|
|
|
|
if (visible) {
|
|
|
|
|
timeoutMs = 200;
|
|
|
|
|
if (softkeyDeadlineMs > 0) {
|
|
|
|
|
timeoutMs = static_cast<int>(std::max<int64_t>(5, softkeyDeadlineMs - nowMs()));
|
|
|
|
|
} else if (zeroHeld) {
|
|
|
|
|
timeoutMs = 25;
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-21 11:57:23 +03:00
|
|
|
|
|
|
|
|
struct pollfd pfds[2] {};
|
|
|
|
|
pfds[0].fd = xfd;
|
|
|
|
|
pfds[0].events = POLLIN;
|
|
|
|
|
pfds[1].fd = fifoFd;
|
|
|
|
|
pfds[1].events = POLLIN;
|
|
|
|
|
::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs);
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
hideWindow(false);
|
|
|
|
|
if (fifoFd >= 0) {
|
|
|
|
|
::close(fifoFd);
|
|
|
|
|
}
|
|
|
|
|
::unlink(getPluginFifoPath("dialer").c_str());
|
|
|
|
|
::unlink(daemonPidPath.c_str());
|
|
|
|
|
|
2026-10-21 11:11:45 +03:00
|
|
|
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);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 11:57:23 +03:00
|
|
|
int dialer_plugin_init(const DwmHostApi* host) {
|
|
|
|
|
if (host && host->spawn_plugin) {
|
|
|
|
|
host->spawn_plugin("dialer", "--preload");
|
|
|
|
|
}
|
2026-10-21 11:11:45 +03:00
|
|
|
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'};
|
2026-10-21 11:57:23 +03:00
|
|
|
if (host->spawn_plugin) {
|
|
|
|
|
host->spawn_plugin("dialer", buf);
|
2026-10-21 11:11:45 +03:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
};
|