Files
bananarchy/src/dwm-plugins/calllog_plugin.cpp
T

699 lines
22 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0-only
// CallLog DWM Plugin - Preloaded resident X11 call log viewer & dialer launcher (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;
}
int calllog_run_main(int argc, char** argv) {
gRunning = 1;
bool isPreload = false;
for (int i = 1; i < argc; ++i) {
if (argv[i] && std::strcmp(argv[i], "--preload") == 0) {
isPreload = true;
}
}
if (sendPluginCommand("calllog", isPreload ? "NOP" : "OPEN")) {
return 0;
}
std::string daemonPidPath = getPluginDaemonPidPath("calllog");
std::string activePidPath = getRuntimeFile("xcalllog.pid");
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
int fifoFd = setupPluginDaemonFifo("calllog");
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) {
if (fifoFd >= 0) ::close(fifoFd);
::unlink(daemonPidPath.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[] = "xcalllog";
char resClass[] = "XCallLog";
ch.res_name = resName;
ch.res_class = resClass;
XSetClassHint(dpy, win, &ch);
XStoreName(dpy, win, "Call Log");
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* layoutMain = pango_layout_new(pangoCtx);
PangoLayout* layoutSmall = pango_layout_new(pangoCtx);
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
PluginTheme theme;
theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon,
nullptr, win);
XftColor& colWhite = theme.colWhite;
XftColor& colLight = theme.colLight;
XftColor& colMuted = theme.colMuted;
XftColor& colYellow = theme.colYellow;
XftColor& colGreen = theme.colGreen;
XftColor& colBlue = theme.colBlue;
XftColor& colRed = theme.colRed;
ContactBook book;
std::vector<CallLogItem> allLogs;
std::vector<CallLogItem> filtered;
FileStamp contactsStamp;
FileStamp logStamp;
int filterTab = 0; // 0 = All, 1 = Missed, 2 = In, 3 = Out
int selected = 0;
bool visible = false;
int64_t openStartMs = 0;
int64_t startupIgnoreUntilMs = 0;
bool initialSoftkeysReleased = true;
int softkeyPending = 0;
int64_t softkeyDeadlineMs = 0;
auto applyFilter = [&]() {
filtered.clear();
for (const auto& item : allLogs) {
if (filterTab == 1 && item.direction != CallDirection::Missed) continue;
if (filterTab == 2 && item.direction != CallDirection::Incoming) continue;
if (filterTab == 3 && item.direction != CallDirection::Outgoing) continue;
filtered.push_back(item);
}
if (selected >= static_cast<int>(filtered.size())) {
selected = std::max(0, static_cast<int>(filtered.size()) - 1);
}
};
bool logsLoadedOnce = false;
auto reloadLogsIfNeeded = [&]() -> bool {
bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath());
bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath());
if (cChanged || lChanged || !logsLoadedOnce) {
logsLoadedOnce = true;
book.load();
allLogs = loadCallLog(book);
applyFilter();
return true;
}
return false;
};
reloadLogsIfNeeded();
applyFilter();
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 = 34;
constexpr int kSoftbarH = 26;
constexpr int kListY = kHeaderH;
constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH;
constexpr int kVisibleRows = 5;
const char* kTabLabels[4] = {"All", "Missed", "In", "Out"};
auto render = [&]() {
XSetForeground(dpy, gc, theme.spec.bgMain);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
XSetForeground(dpy, gc, theme.spec.bgHeader);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
for (int t = 0; t < 4; ++t) {
int tx = (t * kWinWidth) / 4;
int tw = ((t + 1) * kWinWidth) / 4 - tx;
bool active = (t == filterTab);
if (active) {
XSetForeground(dpy, gc, theme.spec.selBg);
XFillRectangle(dpy, pix, gc, tx, 0, tw, kHeaderH - 2);
XSetForeground(dpy, gc, theme.spec.accent);
XFillRectangle(dpy, pix, gc, tx, kHeaderH - 3, tw, 3);
}
std::string lbl(kTabLabels[t]);
int lw = pangoWidth(layoutSmall, lbl);
drawPango(layoutSmall, active ? &colWhite : &colMuted,
tx + (tw - lw) / 2, 0, kHeaderH - 2, lbl);
}
XSetForeground(dpy, gc, theme.spec.border);
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1);
if (filtered.empty()) {
drawPango(layoutIcon, &colMuted, 112, kListY + 60, 24, kIconPhoneLog);
drawPango(layoutMain, &colMuted, 52, kListY + 90, 28, "No call history", 160);
drawPango(layoutSmall, &colMuted, 24, kListY + 120, 20,
"Press 0-9 or Left Softkey to dial", 200);
} else {
int total = static_cast<int>(filtered.size());
int scrollOffset = 0;
if (selected >= kVisibleRows) {
scrollOffset = selected - 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 ? theme.spec.selBg : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd));
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH);
if (isSel) {
XSetForeground(dpy, gc, theme.spec.accent);
XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH);
}
const CallLogItem& item = filtered[idx];
const char* dirIcon = kIconInCall;
XftColor* dirCol = &colGreen;
const char* dirName = "In";
if (item.direction == CallDirection::Outgoing) {
dirIcon = kIconOutCall;
dirCol = &colBlue;
dirName = "Out";
} else if (item.direction == CallDirection::Missed) {
dirIcon = kIconMissedCall;
dirCol = &colRed;
dirName = "Missed";
}
drawPango(layoutIcon, dirCol, 8, ry, rowH, dirIcon);
std::string dur = formatDuration(item.durationSec);
int durW = 0;
if (!dur.empty()) {
durW = pangoWidth(layoutSmall, dur);
drawPango(layoutSmall, isSel ? &colYellow : &colGreen,
kWinWidth - durW - 6, ry + 3, 22, dur);
}
std::string primary = item.contactName.empty() ? item.number : item.contactName;
XftColor* priCol = (item.direction == CallDirection::Missed && !isSel)
? &colRed
: (isSel ? &colWhite : &colLight);
int priMaxW = (durW > 0) ? (kWinWidth - 36 - durW - 6) : 200;
drawPango(layoutMain, priCol, 30, ry + 3, 22, primary, priMaxW);
int timeW = pangoWidth(layoutSmall, item.displayTime);
drawPango(layoutSmall, isSel ? &colWhite : &colMuted,
kWinWidth - timeW - 6, ry + 24, 19, item.displayTime);
std::string secondary =
!item.contactName.empty() ? item.number : std::string(dirName);
int secMaxW = std::max(60, kWinWidth - 36 - timeW - 8);
drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, 19,
secondary, secMaxW);
}
if (total > kVisibleRows) {
int sbH = std::max(16, (kListH * kVisibleRows) / total);
int sbY = kListY +
((kListH - sbH) * scrollOffset) / (total - kVisibleRows);
XSetForeground(dpy, gc, theme.spec.divider);
XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH);
}
}
int sbY = kWinHeight - kSoftbarH;
XSetForeground(dpy, gc, theme.spec.bgFooter);
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH);
XSetForeground(dpy, gc, theme.spec.border);
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1);
drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, "Dialer");
std::string centerLabel = "CALL";
int cw = pangoWidth(layoutMain, centerLabel);
drawPango(layoutMain, filtered.empty() ? &colMuted : &colGreen,
(kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, centerLabel);
std::string rightLabel = "Close";
int rw = pangoWidth(layoutMain, rightLabel);
drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1,
kSoftbarH - 1, rightLabel);
if (visible) {
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
XFlush(dpy);
}
};
// Pre-warm Pango/FreeType/Xrender glyph caches into offscreen Pixmap at startup
drawPango(layoutIcon, &colGreen, 0, 0, 20,
std::string(kIconInCall) + kIconOutCall + kIconMissedCall + kIconPhoneLog);
render();
XSync(dpy, False);
auto showWindow = [&]() {
writeIntFile(activePidPath, static_cast<int>(::getpid()));
closeOtherPlugins("calllog");
bool themeChanged = theme.checkAndReload(
dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon,
nullptr, win);
if (!visible) {
openStartMs = nowMs();
startupIgnoreUntilMs = openStartMs + 90;
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
softkeyPending = 0;
softkeyDeadlineMs = 0;
visible = true;
XMapRaised(dpy, win);
if (!themeChanged) {
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
}
grabPluginKeyboard(dpy, win);
if (reloadLogsIfNeeded() || themeChanged || filterTab != 0 || selected != 0) {
filterTab = 0;
selected = 0;
applyFilter();
render();
}
XFlush(dpy);
} else {
XRaiseWindow(dpy, win);
grabPluginKeyboard(dpy, win);
if (reloadLogsIfNeeded() || themeChanged) {
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;
if (filterTab != 0 || selected != 0) {
filterTab = 0;
selected = 0;
applyFilter();
render();
XFlush(dpy);
}
};
if (!isPreload) {
showWindow();
}
int xfd = ConnectionNumber(dpy);
while (gRunning) {
for (const std::string& cmd : readFifoLines(fifoFd)) {
if (cmd == "NOP") {
continue;
} else if (cmd == "CLOSE") {
hideWindow(false);
} else if (cmd.rfind("OPEN", 0) == 0) {
showWindow();
} else if (cmd == "RELOAD_THEME") {
theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall,
layoutIcon, nullptr, win);
render();
}
}
if (visible && !initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
initialSoftkeysReleased = true;
}
while (XPending(dpy)) {
XEvent ev;
XNextEvent(dpy, &ev);
if (!visible) {
continue;
}
if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
initialSoftkeysReleased = true;
}
if (ev.type == Expose) {
if (ev.xexpose.count == 0) {
render();
}
} else if (ev.type == VisibilityNotify &&
ev.xvisibility.state != VisibilityUnobscured) {
if (isAnotherPluginActive("calllog")) {
hideWindow(false);
} else {
XRaiseWindow(dpy, win);
grabPluginKeyboard(dpy, win);
render();
}
} 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) {
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);
hideWindow(false);
if (!sendPluginCommand("xdrawer", "TOGGLE")) {
runAsyncArgv("xdrawer");
}
break;
}
if (softkeyDeadlineMs == 0) {
softkeyDeadlineMs = nowMs() + 40;
}
continue;
}
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
if (dialCh != '\0') {
std::string arg(1, dialCh);
hideWindow(false);
if (!sendPluginCommand("dialer", "OPEN " + arg)) {
runAsyncArgv("xdialer", arg.c_str());
}
break;
}
switch (sym) {
case XK_Escape:
case XK_BackSpace:
case XK_q:
writeSoftkeySuppress(nowMs() + 300);
hideWindow(true);
break;
case XK_Left:
case XK_KP_Left:
case XK_h:
filterTab = (filterTab + 3) % 4;
selected = 0;
applyFilter();
render();
break;
case XK_Right:
case XK_KP_Right:
case XK_l:
filterTab = (filterTab + 1) % 4;
selected = 0;
applyFilter();
render();
break;
case XK_Up:
case XK_KP_Up:
case XK_k:
if (!filtered.empty()) {
selected = (selected + static_cast<int>(filtered.size()) - 1) %
static_cast<int>(filtered.size());
render();
}
break;
case XK_Down:
case XK_KP_Down:
case XK_j:
if (!filtered.empty()) {
selected = (selected + 1) % static_cast<int>(filtered.size());
render();
}
break;
case XK_Return:
case XK_KP_Enter:
case XF86XK_Phone:
case XF86XK_PickupPhone:
if (nowMs() < startupIgnoreUntilMs) {
break;
}
if (!filtered.empty() && selected >= 0 &&
selected < static_cast<int>(filtered.size())) {
std::string num = filtered[selected].number;
writeSoftkeySuppress(nowMs() + 350);
hideWindow(true);
placePhoneCall(num);
} else if (filtered.empty()) {
hideWindow(false);
if (!sendPluginCommand("dialer", "OPEN")) {
runAsyncArgv("xdialer");
}
}
break;
default:
break;
}
}
}
if (visible && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) {
int comb = softkeyPending | querySoftkeysDown(dpy);
softkeyPending = 0;
softkeyDeadlineMs = 0;
if (comb == 3) {
writeSoftkeySuppress(nowMs() + 500);
hideWindow(false);
if (!sendPluginCommand("xdrawer", "TOGGLE")) {
runAsyncArgv("xdrawer");
}
} else if (comb == 1) {
std::string arg;
if (!filtered.empty() && selected >= 0 &&
selected < static_cast<int>(filtered.size())) {
arg = filtered[selected].number;
}
hideWindow(false);
if (!sendPluginCommand("dialer", arg.empty() ? "OPEN" : ("OPEN " + arg))) {
if (arg.empty()) {
runAsyncArgv("xdialer");
} else {
runAsyncArgv("xdialer", arg.c_str());
}
}
} else if (comb == 2) {
writeSoftkeySuppress(nowMs() + 350);
hideWindow(true);
}
}
int timeoutMs = -1;
if (visible) {
timeoutMs = 200;
if (softkeyDeadlineMs > 0) {
timeoutMs = static_cast<int>(std::max<int64_t>(5, softkeyDeadlineMs - nowMs()));
}
}
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);
}
hideWindow(false);
if (fifoFd >= 0) {
::close(fifoFd);
}
::unlink(getPluginFifoPath("calllog").c_str());
::unlink(daemonPidPath.c_str());
g_object_unref(layoutIcon);
g_object_unref(layoutSmall);
g_object_unref(layoutMain);
g_object_unref(pangoCtx);
XftDrawDestroy(xftDraw);
XFreeGC(dpy, gc);
XFreePixmap(dpy, pix);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
return 0;
}
int calllog_plugin_init(const DwmHostApi* host) {
if (host && host->spawn_plugin) {
host->spawn_plugin("calllog", "--preload");
}
return 0;
}
int calllog_on_global_key(const DwmHostApi* host, KeySym keysym,
unsigned int keycode, unsigned int state,
int is_press) {
(void)host;
if (!is_press || state != 0) {
return 0;
}
if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) {
if (isCallRingingFast()) {
if (!sendPluginCommand("callscreen", "ACCEPT")) {
runAsyncArgv("xcallscreen", "accept");
}
return 1;
}
if (isCallActiveOrRingingFast()) {
if (!sendPluginCommand("callscreen", "OPEN")) {
runAsyncArgv("xcallscreen");
}
return 1;
}
}
return 0;
}
int calllog_on_idle_key(const DwmHostApi* host, KeySym keysym,
unsigned int keycode, unsigned int state) {
if (!host || !host->is_idle_desktop || !host->is_idle_desktop() || state != 0) {
return 0;
}
if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) {
if (isCallRingingFast()) {
if (!sendPluginCommand("callscreen", "ACCEPT")) {
runAsyncArgv("xcallscreen", "accept");
}
return 1;
}
if (isCallActiveOrRingingFast()) {
if (!sendPluginCommand("callscreen", "OPEN")) {
runAsyncArgv("xcallscreen");
}
return 1;
}
if (host->spawn_plugin) {
host->spawn_plugin("calllog", nullptr);
}
return 1;
}
return 0;
}
} // namespace
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
DWM_PLUGIN_API_VERSION,
"calllog",
"Keypad-driven call log viewer & dialer launcher (opens on idle Call key)",
calllog_plugin_init,
nullptr,
nullptr,
calllog_on_idle_key,
calllog_on_global_key,
calllog_run_main
};