feat(dwm): add dynamic plugin system with xdrawer, dialer, and calllog plugins
This commit is contained in:
@@ -1 +1 @@
|
|||||||
xdrawer
|
dwm-plugin
|
||||||
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -155,6 +155,7 @@ $icon_grd Apps^0^sxmo_appmenu.sh applications
|
|||||||
$icon_grd Scripts^0^sxmo_appmenu.sh scripts
|
$icon_grd Scripts^0^sxmo_appmenu.sh scripts
|
||||||
$icon_dir Files^0^sxmo_files.sh
|
$icon_dir Files^0^sxmo_files.sh
|
||||||
$icon_phn Dialer^0^sxmo_modemdial.sh
|
$icon_phn Dialer^0^sxmo_modemdial.sh
|
||||||
|
$icon_phl Call Log^0^xcalllog
|
||||||
$icon_msg Texts^0^sxmo_modemtext.sh
|
$icon_msg Texts^0^sxmo_modemtext.sh
|
||||||
$icon_usr Contacts^0^sxmo_contactmenu.sh
|
$icon_usr Contacts^0^sxmo_contactmenu.sh
|
||||||
${_bt_line}$icon_cam Camera^0^bananarchy-photo --menu
|
${_bt_line}$icon_cam Camera^0^bananarchy-photo --menu
|
||||||
|
|||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# Copyright 2022 Sxmo Contributors
|
||||||
|
|
||||||
|
# include common definitions
|
||||||
|
# shellcheck source=scripts/core/sxmo_common.sh
|
||||||
|
. sxmo_common.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
dial_number() {
|
||||||
|
|
||||||
|
# on pinephone if you attempt to make a call while in a call, modem crashes
|
||||||
|
if sxmo_modemcall.sh list_active_calls | grep -q .; then
|
||||||
|
sxmo_notify_user.sh "Cannot make call while in a call (for now)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
NUMBER="$1"
|
||||||
|
|
||||||
|
CLEANEDNUMBER="$(pnc find ${DEFAULT_COUNTRY:+-c "$DEFAULT_COUNTRY"} "$1" 2>/dev/null || true)"
|
||||||
|
if [ -n "$CLEANEDNUMBER" ] && [ "$NUMBER" != "$CLEANEDNUMBER" ]; then
|
||||||
|
NUMBER="$(cat <<EOF | sxmo_dmenu.sh -p "Rewrite ?"
|
||||||
|
$NUMBER
|
||||||
|
$CLEANEDNUMBER
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sxmo_log "Attempting to dial: $NUMBER"
|
||||||
|
|
||||||
|
if ! CALLID="$(
|
||||||
|
mmcli -m any --voice-create-call "number=$NUMBER" |
|
||||||
|
grep -Eo "Call/[0-9]+" |
|
||||||
|
grep -oE "[0-9]+"
|
||||||
|
)"; then
|
||||||
|
sxmo_notify_user.sh --urgency=critical "We failed to initiate call"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# cleanup all dangling event files, ignore errors
|
||||||
|
rm "$XDG_RUNTIME_DIR/sxmo_calls/$CALLID."* 2>/dev/null || true
|
||||||
|
sxmo_log "Starting call with CALLID: $CALLID"
|
||||||
|
|
||||||
|
sxmo_log "Invoking dial hook"
|
||||||
|
sxmo_hook_dial.sh
|
||||||
|
|
||||||
|
if ! sxmo_modemaudio.sh setup_audio; then
|
||||||
|
sxmo_notify_user.sh --urgency=critical "We failed to setup call audio"
|
||||||
|
mmcli -m any --voice-delete-call="$CALLID"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! sxmo_modemcall.sh pickup "$CALLID"; then
|
||||||
|
sxmo_modemaudio.sh reset_audio
|
||||||
|
mmcli -m any --voice-delete-call="$CALLID"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sxmo_jobs.sh start proximity_lock sxmo_proximitylock.sh
|
||||||
|
sxmo_hook_statusbar.sh state &
|
||||||
|
|
||||||
|
sxmo_jobs.sh start incall_menu sxmo_modemcall.sh incall_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
dial_ussd() {
|
||||||
|
|
||||||
|
USSD="$1"
|
||||||
|
DBUS_MODEM=$(mmcli -J -L | jq -r '."modem-list"[]')
|
||||||
|
MM_BUS="org.freedesktop.ModemManager1"
|
||||||
|
USSD_IFACE="org.freedesktop.ModemManager1.Modem.Modem3gpp.Ussd"
|
||||||
|
|
||||||
|
sxmo_log "Dialling USSD: $USSD"
|
||||||
|
OUTPUT=$(busctl --json=short call "$MM_BUS" "$DBUS_MODEM" "$USSD_IFACE" Initiate "s" "$USSD" | jq -r '.data[]')
|
||||||
|
|
||||||
|
sxmo_terminal.sh sxmo_ussd.sh "$DBUS_MODEM" "Unknown" "$OUTPUT" &
|
||||||
|
}
|
||||||
|
|
||||||
|
dial_menu() {
|
||||||
|
if [ -n "${DISPLAY:-}" ] && command -v xdialer >/dev/null 2>&1; then
|
||||||
|
exec xdialer
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initial menu with recently contacted people
|
||||||
|
NUMBER="$(
|
||||||
|
grep . <<EOF | sxmo_dmenu.sh -p Number -i
|
||||||
|
Close Menu
|
||||||
|
More contacts
|
||||||
|
$(sxmo_contacts.sh --no-groups)
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
# Submenu with all contacts
|
||||||
|
if [ "$NUMBER" = "More contacts" ]; then
|
||||||
|
NUMBER="$(
|
||||||
|
grep . <<EOF | sxmo_dmenu.sh -p Number -i
|
||||||
|
Close Menu
|
||||||
|
$(sxmo_contacts.sh --all --no-groups)
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
# USSD submenu
|
||||||
|
elif [ "$NUMBER" = "Dial USSD" ]; then
|
||||||
|
NUMBER="$(
|
||||||
|
grep . <<EOF | sxmo_dmenu.sh -p Number -i
|
||||||
|
Close Menu
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
dial_ussd "$NUMBER"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
NUMBER="$(printf "%s\n" "$NUMBER" | cut -d: -f2 | tr -d -- '- ')"
|
||||||
|
if [ -z "$NUMBER" ] || [ "$NUMBER" = "CloseMenu" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
dial_number "$NUMBER"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "${1:-}" ]; then
|
||||||
|
case "$1" in
|
||||||
|
\**\#|\#*\#)
|
||||||
|
dial_ussd "$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
dial_number "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
dial_menu
|
||||||
|
fi
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
dwm-plugin
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
dwm-plugin
|
||||||
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
dwm-plugin
|
||||||
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1 +1 @@
|
|||||||
xdrawer
|
dwm-plugin
|
||||||
Binary file not shown.
Executable
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
dwm-plugin
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
dwm-plugin
|
||||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
dwm-plugin
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+51
-13
@@ -88,30 +88,68 @@ EOF
|
|||||||
chmod 755 /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu_path /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu_run
|
chmod 755 /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu_path /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu_run
|
||||||
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu /bananarchy/prebuilt/sxmo-x11/usr/bin/stest
|
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu /bananarchy/prebuilt/sxmo-x11/usr/bin/stest
|
||||||
|
|
||||||
echo "[5/5] Compiling patched sxmo-dwm (240x320 + Fira Code + Pango statusbar + Nokia keypad)..."
|
echo "[5/6] Compiling patched sxmo-dwm (240x320 + Fira Code + Pango statusbar + Plugin API)..."
|
||||||
cd /bananarchy/src/sxmo-dwm
|
cd /bananarchy/src/sxmo-dwm
|
||||||
cp -f config.def.h config.h
|
cp -f config.def.h config.h
|
||||||
DWM_INCS="$(pkgconf --cflags x11 xinerama xft fontconfig freetype2 pango pangoxft xcb xcb-res)"
|
DWM_INCS="$(pkgconf --cflags x11 xinerama xft fontconfig freetype2 pango pangoxft xcb xcb-res)"
|
||||||
DWM_LIBS="$(pkgconf --libs x11 xinerama xft fontconfig freetype2 pango pangoxft xcb xcb-res) -lX11-xcb -lrt"
|
DWM_LIBS="$(pkgconf --libs x11 xinerama xft fontconfig freetype2 pango pangoxft xcb xcb-res) -lX11-xcb -lrt -ldl"
|
||||||
$CC $OPT_CFLAGS -std=c99 -Wno-deprecated-declarations -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=2 -DVERSION=\"6.3.1-bananarchy\" -DXINERAMA -DNOBONSAI $DWM_INCS \
|
$CC $OPT_CFLAGS -std=c99 -Wno-deprecated-declarations -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=2 -DVERSION=\"6.3.1-bananarchy\" -DXINERAMA -DNOBONSAI $DWM_INCS \
|
||||||
dwm.c drw.c util.c $DWM_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
|
dwm.c drw.c util.c $DWM_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
|
||||||
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
|
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm
|
||||||
|
|
||||||
echo "[6/6] Compiling XDrawer (240x320 global phone controls drawer)..."
|
echo "[6/6] Compiling DWM Plugins (xdrawer.so, dialer.so, calllog.so) and dwm-plugin runner..."
|
||||||
cd /bananarchy/src/xdrawer
|
mkdir -p /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins
|
||||||
XDRAWER_INCS="$(pkgconf --cflags x11 xft fontconfig freetype2 pango pangoxft alsa)"
|
PLUG_INCS="$(pkgconf --cflags x11 xft fontconfig freetype2 pango pangoxft alsa)"
|
||||||
XDRAWER_LIBS="$(pkgconf --libs x11 xft fontconfig freetype2 pango pangoxft alsa)"
|
PLUG_LIBS="$(pkgconf --libs x11 xft fontconfig freetype2 pango pangoxft alsa)"
|
||||||
$CXX $OPT_CFLAGS -std=c++17 -Wall -Wextra $XDRAWER_INCS \
|
|
||||||
xdrawer.cpp $XDRAWER_LIBS -o /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
|
|
||||||
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
|
|
||||||
ln -sf xdrawer /bananarchy/prebuilt/sxmo-x11/usr/bin/XDrawer
|
|
||||||
|
|
||||||
mkdir -p /bananarchy/overlay/rootfs/usr/bin
|
$CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \
|
||||||
|
/bananarchy/src/xdrawer/xdrawer.cpp $PLUG_LIBS \
|
||||||
|
-o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so
|
||||||
|
|
||||||
|
$CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \
|
||||||
|
/bananarchy/src/dwm-plugins/dialer_plugin.cpp $PLUG_LIBS \
|
||||||
|
-o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so
|
||||||
|
|
||||||
|
$CXX $OPT_CFLAGS -fPIC -shared -std=c++17 -Wall -Wextra $PLUG_INCS \
|
||||||
|
/bananarchy/src/dwm-plugins/calllog_plugin.cpp $PLUG_LIBS \
|
||||||
|
-o /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so
|
||||||
|
|
||||||
|
$STRIP --strip-unneeded \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/xdrawer.so \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/dialer.so \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/calllog.so
|
||||||
|
|
||||||
|
$CC $OPT_CFLAGS -std=c99 -D_DEFAULT_SOURCE -Wall -Wextra \
|
||||||
|
/bananarchy/src/dwm-plugins/dwm_plugin_runner.c -ldl \
|
||||||
|
-o /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm-plugin
|
||||||
|
$STRIP --strip-unneeded /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm-plugin
|
||||||
|
|
||||||
|
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
|
||||||
|
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/XDrawer
|
||||||
|
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xdialer
|
||||||
|
ln -sf dwm-plugin /bananarchy/prebuilt/sxmo-x11/usr/bin/xcalllog
|
||||||
|
|
||||||
|
mkdir -p /bananarchy/overlay/rootfs/usr/bin /bananarchy/overlay/rootfs/usr/lib/dwm/plugins
|
||||||
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/
|
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /bananarchy/overlay/rootfs/usr/bin/
|
||||||
|
cp -a /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/. /bananarchy/overlay/rootfs/usr/lib/dwm/plugins/
|
||||||
if [ -d /target-rootfs/usr/bin ]; then
|
if [ -d /target-rootfs/usr/bin ]; then
|
||||||
|
mkdir -p /target-rootfs/usr/lib/dwm/plugins
|
||||||
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /target-rootfs/usr/bin/
|
cp -a /bananarchy/prebuilt/sxmo-x11/usr/bin/. /target-rootfs/usr/bin/
|
||||||
|
cp -a /bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/. /target-rootfs/usr/lib/dwm/plugins/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R "$HOST_UID:$HOST_GID" /bananarchy/prebuilt/sxmo-x11 /bananarchy/overlay/rootfs/usr/bin /bananarchy/src/sxmo-st /bananarchy/src/sxmo-dmenu /bananarchy/src/sxmo-dwm /bananarchy/src/xdrawer
|
chown -R "$HOST_UID:$HOST_GID" \
|
||||||
ls -lh /bananarchy/prebuilt/sxmo-x11/usr/bin/st /bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu /bananarchy/prebuilt/sxmo-x11/usr/bin/dwm /bananarchy/prebuilt/sxmo-x11/usr/bin/xdrawer
|
/bananarchy/prebuilt/sxmo-x11 \
|
||||||
|
/bananarchy/overlay/rootfs/usr/bin \
|
||||||
|
/bananarchy/overlay/rootfs/usr/lib \
|
||||||
|
/bananarchy/src/sxmo-st \
|
||||||
|
/bananarchy/src/sxmo-dmenu \
|
||||||
|
/bananarchy/src/sxmo-dwm \
|
||||||
|
/bananarchy/src/xdrawer \
|
||||||
|
/bananarchy/src/dwm-plugins
|
||||||
|
ls -lh /bananarchy/prebuilt/sxmo-x11/usr/bin/st \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/bin/dmenu \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/bin/dwm \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/bin/dwm-plugin \
|
||||||
|
/bananarchy/prebuilt/sxmo-x11/usr/lib/dwm/plugins/*.so
|
||||||
'
|
'
|
||||||
|
|||||||
@@ -0,0 +1,627 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
// CallLog DWM Plugin - Keypad-driven 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCallRinging() {
|
||||||
|
std::string ringPidFile = getRuntimeFile("sxmo.ring.pid");
|
||||||
|
int pid = readIntFile(ringPidFile, -1);
|
||||||
|
if (pid > 0 && ::kill(pid, 0) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ::system("pidof -s sxmo_RepeatRing.sh >/dev/null 2>&1") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int calllog_run_main(int, char**) {
|
||||||
|
gRunning = 1;
|
||||||
|
int64_t startMs = nowMs();
|
||||||
|
|
||||||
|
std::string pidPath = getRuntimeFile("xcalllog.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[] = "xcalllog";
|
||||||
|
char resClass[] = "XCallLog";
|
||||||
|
ch.res_name = resName;
|
||||||
|
ch.res_class = resClass;
|
||||||
|
XSetClassHint(dpy, win, &ch);
|
||||||
|
XStoreName(dpy, win, "Call Log");
|
||||||
|
|
||||||
|
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* 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");
|
||||||
|
XftColor colRed = allocColor("#e74c3c");
|
||||||
|
|
||||||
|
ContactBook book;
|
||||||
|
book.load();
|
||||||
|
std::vector<CallLogItem> allLogs = loadCallLog(book);
|
||||||
|
std::vector<CallLogItem> filtered;
|
||||||
|
|
||||||
|
int filterTab = 0; // 0 = All, 1 = Missed, 2 = In, 3 = Out
|
||||||
|
int selected = 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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; // 236px
|
||||||
|
constexpr int kVisibleRows = 5;
|
||||||
|
|
||||||
|
const char* kTabLabels[4] = {"All", "Missed", "In", "Out"};
|
||||||
|
|
||||||
|
auto render = [&]() {
|
||||||
|
XSetForeground(dpy, gc, 0x222222);
|
||||||
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
||||||
|
|
||||||
|
// 1. Top Filter Tabs Bar (y = 0..34)
|
||||||
|
XSetForeground(dpy, gc, 0x1c1c1c);
|
||||||
|
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, 0x005577);
|
||||||
|
XFillRectangle(dpy, pix, gc, tx, 0, tw, kHeaderH - 2);
|
||||||
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||||||
|
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, 0x444444);
|
||||||
|
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1);
|
||||||
|
|
||||||
|
// 2. Call Log List (y = 34..270, 5 visible rows)
|
||||||
|
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 ? 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 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, 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);
|
||||||
|
|
||||||
|
drawPango(layoutSmall, &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(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 + 140;
|
||||||
|
bool initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
||||||
|
int softkeyPending = 0;
|
||||||
|
int64_t softkeyDeadlineMs = 0;
|
||||||
|
bool closedByKey = false;
|
||||||
|
std::string numberToCall;
|
||||||
|
std::string dialerInitArg;
|
||||||
|
bool openDialerAfter = false;
|
||||||
|
bool launchXDrawer = false;
|
||||||
|
int xfd = ConnectionNumber(dpy);
|
||||||
|
|
||||||
|
while (gRunning) {
|
||||||
|
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 (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pressing 0-9, *, or # in Call Log switches directly to Dialer with that char
|
||||||
|
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
|
||||||
|
if (dialCh != '\0') {
|
||||||
|
dialerInitArg = std::string(1, dialCh);
|
||||||
|
openDialerAfter = true;
|
||||||
|
closedByKey = true;
|
||||||
|
gRunning = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sym) {
|
||||||
|
case XK_Escape:
|
||||||
|
case XK_BackSpace:
|
||||||
|
case XK_q:
|
||||||
|
writeSoftkeySuppress(nowMs() + 350);
|
||||||
|
closedByKey = true;
|
||||||
|
gRunning = 0;
|
||||||
|
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())) {
|
||||||
|
numberToCall = filtered[selected].number;
|
||||||
|
writeSoftkeySuppress(nowMs() + 400);
|
||||||
|
closedByKey = true;
|
||||||
|
gRunning = 0;
|
||||||
|
} else if (filtered.empty()) {
|
||||||
|
openDialerAfter = true;
|
||||||
|
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: open Dialer pre-populated with selected number
|
||||||
|
if (!filtered.empty() && selected >= 0 &&
|
||||||
|
selected < static_cast<int>(filtered.size())) {
|
||||||
|
dialerInitArg = filtered[selected].number;
|
||||||
|
}
|
||||||
|
openDialerAfter = true;
|
||||||
|
closedByKey = true;
|
||||||
|
gRunning = 0;
|
||||||
|
break;
|
||||||
|
} else if (comb == 2) {
|
||||||
|
// Right Softkey: close Call Log
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct pollfd pfd {};
|
||||||
|
pfd.fd = xfd;
|
||||||
|
pfd.events = POLLIN;
|
||||||
|
::poll(&pfd, 1, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closedByKey && !launchXDrawer && !openDialerAfter) {
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
::unlink(pidPath.c_str());
|
||||||
|
|
||||||
|
if (launchXDrawer) {
|
||||||
|
runAsyncArgv("xdrawer");
|
||||||
|
} else if (openDialerAfter) {
|
||||||
|
if (!dialerInitArg.empty()) {
|
||||||
|
runAsyncArgv("xdialer", dialerInitArg.c_str());
|
||||||
|
} else {
|
||||||
|
runAsyncArgv("xdialer");
|
||||||
|
}
|
||||||
|
} else if (!numberToCall.empty()) {
|
||||||
|
placePhoneCall(numberToCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int calllog_plugin_init(const DwmHostApi*) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int calllog_on_global_key(const DwmHostApi* host, KeySym keysym,
|
||||||
|
unsigned int keycode, unsigned int state,
|
||||||
|
int is_press) {
|
||||||
|
if (!is_press || state != 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (keysym == XF86XK_Phone || keysym == XF86XK_PickupPhone || keycode == 177) {
|
||||||
|
if (isCallRinging()) {
|
||||||
|
const char* const argv[] = {"sxmo_hook_pickup.sh", nullptr};
|
||||||
|
if (host && host->spawn_command) {
|
||||||
|
host->spawn_command(argv);
|
||||||
|
} else {
|
||||||
|
runAsyncArgv("sxmo_hook_pickup.sh");
|
||||||
|
}
|
||||||
|
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 (isCallRinging()) {
|
||||||
|
const char* const argv[] = {"sxmo_hook_pickup.sh", nullptr};
|
||||||
|
if (host->spawn_command) {
|
||||||
|
host->spawn_command(argv);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#include <dirent.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "../sxmo-dwm/dwm_plugin.h"
|
||||||
|
|
||||||
|
static const char *const kPluginDirs[] = {
|
||||||
|
"/home/user/.local/lib/dwm/plugins",
|
||||||
|
"/usr/lib/dwm/plugins",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *resolve_plugin_name_from_argv0(const char *argv0) {
|
||||||
|
const char *base = strrchr(argv0, '/');
|
||||||
|
base = base ? (base + 1) : argv0;
|
||||||
|
if (strcasecmp(base, "xdrawer") == 0) {
|
||||||
|
return "xdrawer";
|
||||||
|
}
|
||||||
|
if (strcasecmp(base, "xdialer") == 0 || strcasecmp(base, "dialer") == 0) {
|
||||||
|
return "dialer";
|
||||||
|
}
|
||||||
|
if (strcasecmp(base, "xcalllog") == 0 || strcasecmp(base, "calllog") == 0) {
|
||||||
|
return "calllog";
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int list_plugins(void) {
|
||||||
|
for (int i = 0; kPluginDirs[i]; ++i) {
|
||||||
|
DIR *dir = opendir(kPluginDirs[i]);
|
||||||
|
if (!dir) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
struct dirent *ent;
|
||||||
|
while ((ent = readdir(dir)) != NULL) {
|
||||||
|
size_t len = strlen(ent->d_name);
|
||||||
|
if (len <= 3 || strcmp(ent->d_name + len - 3, ".so") != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char path[512];
|
||||||
|
snprintf(path, sizeof(path), "%s/%s", kPluginDirs[i], ent->d_name);
|
||||||
|
void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (!h) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const DwmPlugin *plug = (const DwmPlugin *)dlsym(h, DWM_PLUGIN_ENTRY_SYMBOL);
|
||||||
|
if (plug && plug->api_version == DWM_PLUGIN_API_VERSION && plug->name) {
|
||||||
|
printf("%-12s %s (%s)\n",
|
||||||
|
plug->name,
|
||||||
|
plug->description ? plug->description : "",
|
||||||
|
path);
|
||||||
|
}
|
||||||
|
dlclose(h);
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *plugin_name = resolve_plugin_name_from_argv0(argv[0]);
|
||||||
|
int sub_argc = argc;
|
||||||
|
char **sub_argv = argv;
|
||||||
|
|
||||||
|
if (!plugin_name) {
|
||||||
|
if (argc < 2 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
|
||||||
|
fprintf(stderr, "Usage: %s <plugin-name|--list> [args...]\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strcmp(argv[1], "--list") == 0 || strcmp(argv[1], "-l") == 0) {
|
||||||
|
return list_plugins();
|
||||||
|
}
|
||||||
|
plugin_name = argv[1];
|
||||||
|
sub_argc = argc - 1;
|
||||||
|
sub_argv = argv + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *env_dir = getenv("DWM_PLUGIN_PATH");
|
||||||
|
char path[512];
|
||||||
|
void *handle = NULL;
|
||||||
|
|
||||||
|
if (env_dir && env_dir[0]) {
|
||||||
|
snprintf(path, sizeof(path), "%s/%s.so", env_dir, plugin_name);
|
||||||
|
if (access(path, R_OK) == 0) {
|
||||||
|
handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; !handle && kPluginDirs[i]; ++i) {
|
||||||
|
snprintf(path, sizeof(path), "%s/%s.so", kPluginDirs[i], plugin_name);
|
||||||
|
if (access(path, R_OK) == 0) {
|
||||||
|
handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handle) {
|
||||||
|
fprintf(stderr, "dwm-plugin: failed to load plugin '%s': %s\n",
|
||||||
|
plugin_name, dlerror());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DwmPlugin *plug = (const DwmPlugin *)dlsym(handle, DWM_PLUGIN_ENTRY_SYMBOL);
|
||||||
|
if (!plug || plug->api_version != DWM_PLUGIN_API_VERSION || !plug->run_main) {
|
||||||
|
fprintf(stderr, "dwm-plugin: invalid plugin '%s'\n", plugin_name);
|
||||||
|
dlclose(handle);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = plug->run_main(sub_argc, sub_argv);
|
||||||
|
dlclose(handle);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,625 @@
|
|||||||
|
#ifndef DWM_PLUGINS_PHONE_DATA_H
|
||||||
|
#define DWM_PLUGINS_PHONE_DATA_H
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
#include <X11/XF86keysym.h>
|
||||||
|
#ifndef _EVDEVK
|
||||||
|
#define _EVDEVK(_v) (0x10081000 + (_v))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace bananarchy {
|
||||||
|
|
||||||
|
constexpr int kScreenWidth = 240;
|
||||||
|
constexpr int kScreenHeight = 320;
|
||||||
|
constexpr int kTopBarHeight = 24;
|
||||||
|
constexpr int kWinX = 0;
|
||||||
|
constexpr int kWinY = kTopBarHeight;
|
||||||
|
constexpr int kWinWidth = kScreenWidth;
|
||||||
|
constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar
|
||||||
|
|
||||||
|
// Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font")
|
||||||
|
constexpr const char* kIconPhone = "\xef\x82\x95"; // U+F095 phone handset
|
||||||
|
constexpr const char* kIconPhoneLog = "\xef\x82\x98"; // U+F098 phone square
|
||||||
|
constexpr const char* kIconUser = "\xef\x80\x87"; // U+F007 user contact
|
||||||
|
constexpr const char* kIconClock = "\xef\x80\x97"; // U+F017 recent clock
|
||||||
|
constexpr const char* kIconOutCall = "\xef\x85\xb8"; // U+F178 outgoing arrow
|
||||||
|
constexpr const char* kIconInCall = "\xef\x85\xb5"; // U+F175 incoming arrow
|
||||||
|
constexpr const char* kIconMissedCall = "\xef\x81\xb1"; // U+F071 missed/alert
|
||||||
|
constexpr const char* kIconBackspace = "\xef\x86\x90"; // U+F190 left circle arrow
|
||||||
|
|
||||||
|
inline int64_t nowMs() {
|
||||||
|
struct timespec ts {};
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return static_cast<int64_t>(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string getRuntimeFile(const char* name) {
|
||||||
|
const char* rdir = std::getenv("XDG_RUNTIME_DIR");
|
||||||
|
std::string base = (rdir && rdir[0]) ? rdir : "/run/user/10000";
|
||||||
|
if (::access(base.c_str(), W_OK) != 0) {
|
||||||
|
base = "/tmp";
|
||||||
|
}
|
||||||
|
return base + "/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int querySoftkeysDown(Display* d) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool isKeycodeDown(Display* d, int keycode) {
|
||||||
|
if (keycode <= 0 || keycode >= 256) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char km[32];
|
||||||
|
if (!XQueryKeymap(d, km)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (km[keycode / 8] & (1 << (keycode % 8))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void runAsyncArgv(const char* prog, const char* arg1 = nullptr) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arg1) {
|
||||||
|
execlp(prog, prog, arg1, static_cast<char*>(nullptr));
|
||||||
|
std::string localBin = std::string("/home/user/.local/bin/") + prog;
|
||||||
|
execl(localBin.c_str(), prog, arg1, static_cast<char*>(nullptr));
|
||||||
|
std::string usrBin = std::string("/usr/bin/") + prog;
|
||||||
|
execl(usrBin.c_str(), prog, arg1, static_cast<char*>(nullptr));
|
||||||
|
} else {
|
||||||
|
execlp(prog, prog, static_cast<char*>(nullptr));
|
||||||
|
std::string localBin = std::string("/home/user/.local/bin/") + prog;
|
||||||
|
execl(localBin.c_str(), prog, static_cast<char*>(nullptr));
|
||||||
|
std::string usrBin = std::string("/usr/bin/") + prog;
|
||||||
|
execl(usrBin.c_str(), prog, static_cast<char*>(nullptr));
|
||||||
|
}
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline char mapDialKey(KeySym sym, unsigned int keycode, unsigned int state) {
|
||||||
|
if (state & (ControlMask | Mod1Mask | Mod4Mask)) {
|
||||||
|
return '\0';
|
||||||
|
}
|
||||||
|
// Keypad star (*)
|
||||||
|
if (keycode == 63 || keycode == 530 || sym == XK_asterisk ||
|
||||||
|
sym == XK_KP_Multiply || static_cast<uint32_t>(sym) == 0x1008ffaa) {
|
||||||
|
return '*';
|
||||||
|
}
|
||||||
|
// Keypad pound (#)
|
||||||
|
if (keycode == 106 || keycode == 531 || sym == XK_numbersign ||
|
||||||
|
sym == XK_KP_Divide || static_cast<uint32_t>(sym) == 0x1008ffab) {
|
||||||
|
return '#';
|
||||||
|
}
|
||||||
|
if (sym == XK_plus || sym == XK_KP_Add) {
|
||||||
|
return '+';
|
||||||
|
}
|
||||||
|
if (sym >= XK_0 && sym <= XK_9) {
|
||||||
|
return static_cast<char>('0' + (sym - XK_0));
|
||||||
|
}
|
||||||
|
if (sym >= XK_KP_0 && sym <= XK_KP_9) {
|
||||||
|
return static_cast<char>('0' + (sym - XK_KP_0));
|
||||||
|
}
|
||||||
|
if (keycode >= 10 && keycode <= 18) {
|
||||||
|
return static_cast<char>('1' + (keycode - 10));
|
||||||
|
}
|
||||||
|
if (keycode == 19) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string trimStr(const std::string& s) {
|
||||||
|
size_t b = 0;
|
||||||
|
while (b < s.size() && std::isspace(static_cast<unsigned char>(s[b]))) {
|
||||||
|
++b;
|
||||||
|
}
|
||||||
|
size_t e = s.size();
|
||||||
|
while (e > b && std::isspace(static_cast<unsigned char>(s[e - 1]))) {
|
||||||
|
--e;
|
||||||
|
}
|
||||||
|
return s.substr(b, e - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string normalizePhone(const std::string& raw) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(raw.size());
|
||||||
|
for (char c : raw) {
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c)) || c == '+' || c == '*' ||
|
||||||
|
c == '#') {
|
||||||
|
out.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string phoneSuffixKey(const std::string& norm) {
|
||||||
|
std::string digits;
|
||||||
|
for (char c : norm) {
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||||
|
digits.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (digits.size() >= 9) {
|
||||||
|
return digits.substr(digits.size() - 9);
|
||||||
|
}
|
||||||
|
return norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string toLowerStr(const std::string& s) {
|
||||||
|
std::string out = s;
|
||||||
|
for (char& c : out) {
|
||||||
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string toT9Digits(const std::string& name) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(name.size());
|
||||||
|
for (unsigned char uc : name) {
|
||||||
|
char c = static_cast<char>(std::tolower(uc));
|
||||||
|
if (c >= 'a' && c <= 'c') out.push_back('2');
|
||||||
|
else if (c >= 'd' && c <= 'f') out.push_back('3');
|
||||||
|
else if (c >= 'g' && c <= 'i') out.push_back('4');
|
||||||
|
else if (c >= 'j' && c <= 'l') out.push_back('5');
|
||||||
|
else if (c >= 'm' && c <= 'o') out.push_back('6');
|
||||||
|
else if (c >= 'p' && c <= 's') out.push_back('7');
|
||||||
|
else if (c >= 't' && c <= 'v') out.push_back('8');
|
||||||
|
else if (c >= 'w' && c <= 'z') out.push_back('9');
|
||||||
|
else if (c == ' ') out.push_back('0');
|
||||||
|
else if (std::isdigit(uc)) out.push_back(c);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string getContactsFilePath() {
|
||||||
|
const char* env = std::getenv("SXMO_CONTACTFILE");
|
||||||
|
if (env && env[0]) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||||
|
if (xdg && xdg[0]) {
|
||||||
|
return std::string(xdg) + "/sxmo/contacts.tsv";
|
||||||
|
}
|
||||||
|
const char* home = std::getenv("HOME");
|
||||||
|
return std::string(home && home[0] ? home : "/home/user") +
|
||||||
|
"/.config/sxmo/contacts.tsv";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string getModemLogFilePath() {
|
||||||
|
const char* env = std::getenv("SXMO_LOGDIR");
|
||||||
|
if (env && env[0]) {
|
||||||
|
return std::string(env) + "/modemlog.tsv";
|
||||||
|
}
|
||||||
|
const char* xdg = std::getenv("XDG_DATA_HOME");
|
||||||
|
if (xdg && xdg[0]) {
|
||||||
|
return std::string(xdg) + "/sxmo/modem/modemlog.tsv";
|
||||||
|
}
|
||||||
|
const char* home = std::getenv("HOME");
|
||||||
|
return std::string(home && home[0] ? home : "/home/user") +
|
||||||
|
"/.local/share/sxmo/modem/modemlog.tsv";
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContactEntry {
|
||||||
|
std::string name;
|
||||||
|
std::string number;
|
||||||
|
std::string normNumber;
|
||||||
|
std::string t9Name;
|
||||||
|
std::string lowerName;
|
||||||
|
bool isRecent = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ContactBook {
|
||||||
|
public:
|
||||||
|
void load() {
|
||||||
|
saved_.clear();
|
||||||
|
exactMap_.clear();
|
||||||
|
suffixMap_.clear();
|
||||||
|
|
||||||
|
std::ifstream cf(getContactsFilePath());
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(cf, line)) {
|
||||||
|
if (line.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t tab = line.find('\t');
|
||||||
|
if (tab == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string num = trimStr(line.substr(0, tab));
|
||||||
|
std::string name = trimStr(line.substr(tab + 1));
|
||||||
|
std::string norm = normalizePhone(num);
|
||||||
|
if (norm.empty() || name.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ContactEntry e;
|
||||||
|
e.name = name;
|
||||||
|
e.number = num;
|
||||||
|
e.normNumber = norm;
|
||||||
|
e.t9Name = toT9Digits(name);
|
||||||
|
e.lowerName = toLowerStr(name);
|
||||||
|
e.isRecent = false;
|
||||||
|
saved_.push_back(e);
|
||||||
|
exactMap_[norm] = name;
|
||||||
|
std::string sfx = phoneSuffixKey(norm);
|
||||||
|
if (!sfx.empty()) {
|
||||||
|
suffixMap_[sfx] = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(saved_.begin(), saved_.end(),
|
||||||
|
[](const ContactEntry& a, const ContactEntry& b) {
|
||||||
|
return a.lowerName < b.lowerName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string resolveName(const std::string& rawNumber) const {
|
||||||
|
std::string norm = normalizePhone(rawNumber);
|
||||||
|
if (norm.empty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
auto it = exactMap_.find(norm);
|
||||||
|
if (it != exactMap_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
std::string sfx = phoneSuffixKey(norm);
|
||||||
|
if (!sfx.empty()) {
|
||||||
|
auto sit = suffixMap_.find(sfx);
|
||||||
|
if (sit != suffixMap_.end()) {
|
||||||
|
return sit->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ContactEntry> buildDialerList() const {
|
||||||
|
std::vector<ContactEntry> out;
|
||||||
|
std::unordered_set<std::string> seenKey;
|
||||||
|
|
||||||
|
// 1. Recent unique numbers from modemlog.tsv in reverse chronological order
|
||||||
|
std::ifstream lf(getModemLogFilePath());
|
||||||
|
std::vector<std::string> logLines;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(lf, line)) {
|
||||||
|
if (!line.empty()) {
|
||||||
|
logLines.push_back(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto it = logLines.rbegin(); it != logLines.rend(); ++it) {
|
||||||
|
size_t t1 = it->find('\t');
|
||||||
|
if (t1 == std::string::npos) continue;
|
||||||
|
size_t t2 = it->find('\t', t1 + 1);
|
||||||
|
if (t2 == std::string::npos) continue;
|
||||||
|
size_t t3 = it->find('\t', t2 + 1);
|
||||||
|
std::string num = trimStr(
|
||||||
|
t3 == std::string::npos ? it->substr(t2 + 1) : it->substr(t2 + 1, t3 - t2 - 1));
|
||||||
|
if (num.empty() || num == "NULL" || num.find('+', 1) != std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string norm = normalizePhone(num);
|
||||||
|
if (norm.empty()) continue;
|
||||||
|
std::string key = phoneSuffixKey(norm);
|
||||||
|
if (seenKey.count(key) || seenKey.count(norm)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenKey.insert(key);
|
||||||
|
seenKey.insert(norm);
|
||||||
|
|
||||||
|
ContactEntry e;
|
||||||
|
std::string cname = resolveName(num);
|
||||||
|
e.name = cname.empty() ? num : cname;
|
||||||
|
e.number = num;
|
||||||
|
e.normNumber = norm;
|
||||||
|
e.t9Name = toT9Digits(e.name);
|
||||||
|
e.lowerName = toLowerStr(e.name);
|
||||||
|
e.isRecent = true;
|
||||||
|
out.push_back(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. All saved contacts (skipping duplicates already added from recents)
|
||||||
|
for (const auto& c : saved_) {
|
||||||
|
std::string key = phoneSuffixKey(c.normNumber);
|
||||||
|
if (seenKey.count(key) || seenKey.count(c.normNumber)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenKey.insert(key);
|
||||||
|
seenKey.insert(c.normNumber);
|
||||||
|
out.push_back(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ContactEntry> saved_;
|
||||||
|
std::unordered_map<std::string, std::string> exactMap_;
|
||||||
|
std::unordered_map<std::string, std::string> suffixMap_;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CallDirection {
|
||||||
|
Incoming = 0,
|
||||||
|
Outgoing = 1,
|
||||||
|
Missed = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CallLogItem {
|
||||||
|
CallDirection direction;
|
||||||
|
std::string number;
|
||||||
|
std::string contactName;
|
||||||
|
std::string displayTime;
|
||||||
|
int durationSec = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline time_t parseIsoTime(const std::string& iso) {
|
||||||
|
if (iso.size() < 19) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
struct tm tmVal {};
|
||||||
|
tmVal.tm_year = std::atoi(iso.substr(0, 4).c_str()) - 1900;
|
||||||
|
tmVal.tm_mon = std::atoi(iso.substr(5, 2).c_str()) - 1;
|
||||||
|
tmVal.tm_mday = std::atoi(iso.substr(8, 2).c_str());
|
||||||
|
tmVal.tm_hour = std::atoi(iso.substr(11, 2).c_str());
|
||||||
|
tmVal.tm_min = std::atoi(iso.substr(14, 2).c_str());
|
||||||
|
tmVal.tm_sec = std::atoi(iso.substr(17, 2).c_str());
|
||||||
|
tmVal.tm_isdst = -1;
|
||||||
|
return mktime(&tmVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string formatShortTime(const std::string& iso) {
|
||||||
|
if (iso.size() < 16) {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
// Convert "YYYY-MM-DDTHH:MM:SS+0300" -> "MM-DD HH:MM"
|
||||||
|
return iso.substr(5, 5) + " " + iso.substr(11, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string formatDuration(int sec) {
|
||||||
|
if (sec <= 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
char buf[32];
|
||||||
|
if (sec >= 3600) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%dh%02dm", sec / 3600, (sec % 3600) / 60);
|
||||||
|
} else if (sec >= 60) {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%dm%02ds", sec / 60, sec % 60);
|
||||||
|
} else {
|
||||||
|
std::snprintf(buf, sizeof(buf), "%ds", sec);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<CallLogItem> loadCallLog(const ContactBook& book) {
|
||||||
|
std::ifstream lf(getModemLogFilePath());
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
struct ActiveSession {
|
||||||
|
bool active = false;
|
||||||
|
CallDirection dir = CallDirection::Incoming;
|
||||||
|
std::string number;
|
||||||
|
std::string startTimeStr;
|
||||||
|
time_t startEpoch = 0;
|
||||||
|
time_t answerEpoch = 0;
|
||||||
|
bool answered = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<CallLogItem> history;
|
||||||
|
ActiveSession cur;
|
||||||
|
|
||||||
|
auto flushSession = [&]() {
|
||||||
|
if (!cur.active || cur.number.empty()) {
|
||||||
|
cur = ActiveSession{};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CallLogItem item;
|
||||||
|
item.direction = cur.dir;
|
||||||
|
item.number = cur.number;
|
||||||
|
item.contactName = book.resolveName(cur.number);
|
||||||
|
item.displayTime = formatShortTime(cur.startTimeStr);
|
||||||
|
item.durationSec = 0;
|
||||||
|
history.push_back(item);
|
||||||
|
cur = ActiveSession{};
|
||||||
|
};
|
||||||
|
|
||||||
|
while (std::getline(lf, line)) {
|
||||||
|
if (line.empty()) continue;
|
||||||
|
size_t t1 = line.find('\t');
|
||||||
|
if (t1 == std::string::npos) continue;
|
||||||
|
size_t t2 = line.find('\t', t1 + 1);
|
||||||
|
if (t2 == std::string::npos) continue;
|
||||||
|
size_t t3 = line.find('\t', t2 + 1);
|
||||||
|
|
||||||
|
std::string ts = trimStr(line.substr(0, t1));
|
||||||
|
std::string evt = trimStr(line.substr(t1 + 1, t2 - t1 - 1));
|
||||||
|
std::string num = trimStr(
|
||||||
|
t3 == std::string::npos ? line.substr(t2 + 1) : line.substr(t2 + 1, t3 - t2 - 1));
|
||||||
|
|
||||||
|
if (evt.rfind("call_", 0) != 0 && evt != "ring_muted") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (num.empty() || num == "NULL") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t epoch = parseIsoTime(ts);
|
||||||
|
|
||||||
|
if (evt == "call_ring") {
|
||||||
|
flushSession();
|
||||||
|
cur.active = true;
|
||||||
|
cur.dir = CallDirection::Missed;
|
||||||
|
cur.number = num;
|
||||||
|
cur.startTimeStr = ts;
|
||||||
|
cur.startEpoch = epoch;
|
||||||
|
cur.answerEpoch = 0;
|
||||||
|
cur.answered = false;
|
||||||
|
} else if (evt == "call_start") {
|
||||||
|
flushSession();
|
||||||
|
cur.active = true;
|
||||||
|
cur.dir = CallDirection::Outgoing;
|
||||||
|
cur.number = num;
|
||||||
|
cur.startTimeStr = ts;
|
||||||
|
cur.startEpoch = epoch;
|
||||||
|
cur.answerEpoch = epoch;
|
||||||
|
cur.answered = true;
|
||||||
|
} else if (evt == "call_pickup") {
|
||||||
|
if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) {
|
||||||
|
cur.dir = CallDirection::Incoming;
|
||||||
|
cur.answered = true;
|
||||||
|
cur.answerEpoch = epoch;
|
||||||
|
} else {
|
||||||
|
flushSession();
|
||||||
|
cur.active = true;
|
||||||
|
cur.dir = CallDirection::Incoming;
|
||||||
|
cur.number = num;
|
||||||
|
cur.startTimeStr = ts;
|
||||||
|
cur.startEpoch = epoch;
|
||||||
|
cur.answerEpoch = epoch;
|
||||||
|
cur.answered = true;
|
||||||
|
}
|
||||||
|
} else if (evt == "call_missed") {
|
||||||
|
if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) {
|
||||||
|
cur.dir = CallDirection::Missed;
|
||||||
|
flushSession();
|
||||||
|
} else {
|
||||||
|
flushSession();
|
||||||
|
CallLogItem item;
|
||||||
|
item.direction = CallDirection::Missed;
|
||||||
|
item.number = num;
|
||||||
|
item.contactName = book.resolveName(num);
|
||||||
|
item.displayTime = formatShortTime(ts);
|
||||||
|
item.durationSec = 0;
|
||||||
|
history.push_back(item);
|
||||||
|
}
|
||||||
|
} else if (evt == "call_hangup" || evt == "call_finished" ||
|
||||||
|
evt == "call_discard") {
|
||||||
|
if (cur.active && normalizePhone(cur.number) == normalizePhone(num)) {
|
||||||
|
if (evt == "call_discard" && !cur.answered &&
|
||||||
|
cur.dir != CallDirection::Outgoing) {
|
||||||
|
cur.dir = CallDirection::Missed;
|
||||||
|
}
|
||||||
|
CallLogItem item;
|
||||||
|
item.direction = cur.dir;
|
||||||
|
item.number = cur.number;
|
||||||
|
item.contactName = book.resolveName(cur.number);
|
||||||
|
item.displayTime = formatShortTime(cur.startTimeStr);
|
||||||
|
if (cur.answered && cur.answerEpoch > 0 && epoch >= cur.answerEpoch) {
|
||||||
|
item.durationSec = static_cast<int>(epoch - cur.answerEpoch);
|
||||||
|
}
|
||||||
|
history.push_back(item);
|
||||||
|
cur = ActiveSession{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushSession();
|
||||||
|
|
||||||
|
std::reverse(history.begin(), history.end());
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void placePhoneCall(const std::string& rawNumber) {
|
||||||
|
std::string num = normalizePhone(rawNumber);
|
||||||
|
if (num.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runAsyncArgv("sxmo_modemdial.sh", num.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace bananarchy
|
||||||
|
|
||||||
|
#endif // DWM_PLUGINS_PHONE_DATA_H
|
||||||
+208
-5
@@ -271,6 +271,8 @@ static void shiftview(const Arg *arg);
|
|||||||
static void pushup(const Arg *arg);
|
static void pushup(const Arg *arg);
|
||||||
static void pushdown(const Arg *arg);
|
static void pushdown(const Arg *arg);
|
||||||
static void readxresources();
|
static void readxresources();
|
||||||
|
static void load_plugins(void);
|
||||||
|
static void unload_plugins(void);
|
||||||
|
|
||||||
static pid_t getparentprocess(pid_t p);
|
static pid_t getparentprocess(pid_t p);
|
||||||
static int isdescprocess(pid_t p, pid_t c);
|
static int isdescprocess(pid_t p, pid_t c);
|
||||||
@@ -698,6 +700,7 @@ cleanup(void)
|
|||||||
drw_cur_free(drw, cursor[i]);
|
drw_cur_free(drw, cursor[i]);
|
||||||
for (i = 0; i < LENGTH(colors); i++)
|
for (i = 0; i < LENGTH(colors); i++)
|
||||||
free(scheme[i]);
|
free(scheme[i]);
|
||||||
|
unload_plugins();
|
||||||
XDestroyWindow(dpy, wmcheckwin);
|
XDestroyWindow(dpy, wmcheckwin);
|
||||||
drw_free(drw);
|
drw_free(drw);
|
||||||
XSync(dpy, False);
|
XSync(dpy, False);
|
||||||
@@ -1532,6 +1535,191 @@ is_right_softkey(KeySym ks, unsigned int keycode)
|
|||||||
return ks == XF86XK_Back || ks == XF86XK_KbdLcdMenu2 || keycode == 166;
|
return ks == XF86XK_Back || ks == XF86XK_KbdLcdMenu2 || keycode == 166;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include "dwm_plugin.h"
|
||||||
|
|
||||||
|
#define MAX_DWM_PLUGINS 16
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *handle;
|
||||||
|
const DwmPlugin *plugin;
|
||||||
|
} LoadedDwmPlugin;
|
||||||
|
|
||||||
|
static LoadedDwmPlugin loaded_plugins[MAX_DWM_PLUGINS];
|
||||||
|
static int num_loaded_plugins = 0;
|
||||||
|
|
||||||
|
static int
|
||||||
|
host_is_idle_desktop(void)
|
||||||
|
{
|
||||||
|
return (selmon && selmon->sel == NULL && !is_phone_locked()) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
host_is_phone_locked(void)
|
||||||
|
{
|
||||||
|
return is_phone_locked();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
host_spawn_command(const char *const *argv)
|
||||||
|
{
|
||||||
|
if (!argv || !argv[0])
|
||||||
|
return;
|
||||||
|
Arg a = { .v = (const void *)argv };
|
||||||
|
XUngrabKeyboard(dpy, CurrentTime);
|
||||||
|
XFlush(dpy);
|
||||||
|
spawn(&a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
host_spawn_plugin(const char *plugin_name, const char *arg)
|
||||||
|
{
|
||||||
|
if (!plugin_name || !plugin_name[0])
|
||||||
|
return;
|
||||||
|
const char *cmd[4];
|
||||||
|
cmd[0] = "dwm-plugin";
|
||||||
|
cmd[1] = plugin_name;
|
||||||
|
cmd[2] = (arg && arg[0]) ? arg : NULL;
|
||||||
|
cmd[3] = NULL;
|
||||||
|
Arg a = { .v = cmd };
|
||||||
|
XUngrabKeyboard(dpy, CurrentTime);
|
||||||
|
XFlush(dpy);
|
||||||
|
spawn(&a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DwmHostApi dwm_host_api = {
|
||||||
|
.api_version = DWM_PLUGIN_API_VERSION,
|
||||||
|
.dpy = NULL,
|
||||||
|
.root = 0,
|
||||||
|
.screen = 0,
|
||||||
|
.sw = 0,
|
||||||
|
.sh = 0,
|
||||||
|
.bh = 0,
|
||||||
|
.is_idle_desktop = host_is_idle_desktop,
|
||||||
|
.is_phone_locked = host_is_phone_locked,
|
||||||
|
.spawn_command = host_spawn_command,
|
||||||
|
.spawn_plugin = host_spawn_plugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
is_plugin_loaded(const char *name)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < num_loaded_plugins; i++) {
|
||||||
|
if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->name &&
|
||||||
|
strcmp(loaded_plugins[i].plugin->name, name) == 0)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
load_plugins_from_dir(const char *dirpath)
|
||||||
|
{
|
||||||
|
DIR *dir;
|
||||||
|
struct dirent *ent;
|
||||||
|
char path[512];
|
||||||
|
|
||||||
|
if (!dirpath || !dirpath[0])
|
||||||
|
return;
|
||||||
|
dir = opendir(dirpath);
|
||||||
|
if (!dir)
|
||||||
|
return;
|
||||||
|
while ((ent = readdir(dir)) != NULL && num_loaded_plugins < MAX_DWM_PLUGINS) {
|
||||||
|
size_t len = strlen(ent->d_name);
|
||||||
|
if (len <= 3 || strcmp(ent->d_name + len - 3, ".so") != 0)
|
||||||
|
continue;
|
||||||
|
snprintf(path, sizeof(path), "%s/%s", dirpath, ent->d_name);
|
||||||
|
void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (!h)
|
||||||
|
continue;
|
||||||
|
const DwmPlugin *plug = (const DwmPlugin *)dlsym(h, DWM_PLUGIN_ENTRY_SYMBOL);
|
||||||
|
if (!plug || plug->api_version != DWM_PLUGIN_API_VERSION ||
|
||||||
|
!plug->name || is_plugin_loaded(plug->name)) {
|
||||||
|
dlclose(h);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (plug->init && plug->init(&dwm_host_api) != 0) {
|
||||||
|
dlclose(h);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
loaded_plugins[num_loaded_plugins].handle = h;
|
||||||
|
loaded_plugins[num_loaded_plugins].plugin = plug;
|
||||||
|
num_loaded_plugins++;
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
load_plugins(void)
|
||||||
|
{
|
||||||
|
dwm_host_api.dpy = dpy;
|
||||||
|
dwm_host_api.root = root;
|
||||||
|
dwm_host_api.screen = screen;
|
||||||
|
dwm_host_api.sw = sw;
|
||||||
|
dwm_host_api.sh = sh;
|
||||||
|
dwm_host_api.bh = bh;
|
||||||
|
|
||||||
|
const char *env_dir = getenv("DWM_PLUGIN_PATH");
|
||||||
|
if (env_dir && env_dir[0])
|
||||||
|
load_plugins_from_dir(env_dir);
|
||||||
|
load_plugins_from_dir("/home/user/.local/lib/dwm/plugins");
|
||||||
|
load_plugins_from_dir("/usr/lib/dwm/plugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
unload_plugins(void)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < num_loaded_plugins; i++) {
|
||||||
|
if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->cleanup)
|
||||||
|
loaded_plugins[i].plugin->cleanup();
|
||||||
|
if (loaded_plugins[i].handle)
|
||||||
|
dlclose(loaded_plugins[i].handle);
|
||||||
|
}
|
||||||
|
num_loaded_plugins = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
dispatch_plugin_softkey_chord(void)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < num_loaded_plugins; i++) {
|
||||||
|
if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->on_softkey_chord) {
|
||||||
|
if (loaded_plugins[i].plugin->on_softkey_chord(&dwm_host_api))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
dispatch_plugin_idle_key(KeySym keysym, unsigned int keycode, unsigned int state)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < num_loaded_plugins; i++) {
|
||||||
|
if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->on_idle_key) {
|
||||||
|
if (loaded_plugins[i].plugin->on_idle_key(&dwm_host_api, keysym, keycode, state))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
dispatch_plugin_global_key(KeySym keysym, unsigned int keycode, unsigned int state, int is_press)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < num_loaded_plugins; i++) {
|
||||||
|
if (loaded_plugins[i].plugin && loaded_plugins[i].plugin->on_global_key) {
|
||||||
|
if (loaded_plugins[i].plugin->on_global_key(&dwm_host_api, keysym, keycode, state, is_press))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static pid_t
|
static pid_t
|
||||||
spawn_pgid(const Arg *arg)
|
spawn_pgid(const Arg *arg)
|
||||||
{
|
{
|
||||||
@@ -1547,9 +1735,8 @@ spawn_pgid(const Arg *arg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
trigger_xdrawer_chord(long long now)
|
trigger_softkey_chord(long long now)
|
||||||
{
|
{
|
||||||
const Arg xdrawercmd = { .v = (const char*[]){ "xdrawer", NULL } };
|
|
||||||
if (last_menu_pgid > 0) {
|
if (last_menu_pgid > 0) {
|
||||||
kill(-last_menu_pgid, SIGKILL);
|
kill(-last_menu_pgid, SIGKILL);
|
||||||
last_menu_pgid = 0;
|
last_menu_pgid = 0;
|
||||||
@@ -1563,8 +1750,11 @@ trigger_xdrawer_chord(long long now)
|
|||||||
write_softkey_suppress(now + SOFTKEY_SUPPRESS_MS);
|
write_softkey_suppress(now + SOFTKEY_SUPPRESS_MS);
|
||||||
XUngrabKeyboard(dpy, CurrentTime);
|
XUngrabKeyboard(dpy, CurrentTime);
|
||||||
XFlush(dpy);
|
XFlush(dpy);
|
||||||
|
if (!dispatch_plugin_softkey_chord()) {
|
||||||
|
const Arg xdrawercmd = { .v = (const char*[]){ "xdrawer", NULL } };
|
||||||
spawn(&xdrawercmd);
|
spawn(&xdrawercmd);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
flush_pending_softkey(void)
|
flush_pending_softkey(void)
|
||||||
@@ -1631,7 +1821,7 @@ keypress(XEvent *e)
|
|||||||
if (last_softkey_mask != 0 && (now - last_softkey_ms) <= SOFTKEY_CANCEL_WINDOW_MS)
|
if (last_softkey_mask != 0 && (now - last_softkey_ms) <= SOFTKEY_CANCEL_WINDOW_MS)
|
||||||
combined |= last_softkey_mask;
|
combined |= last_softkey_mask;
|
||||||
if (combined == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
|
if (combined == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
|
||||||
trigger_xdrawer_chord(now);
|
trigger_softkey_chord(now);
|
||||||
} else {
|
} else {
|
||||||
softkey_pending = which;
|
softkey_pending = which;
|
||||||
last_softkey_mask = which;
|
last_softkey_mask = which;
|
||||||
@@ -1645,6 +1835,16 @@ keypress(XEvent *e)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!is_phone_locked() &&
|
||||||
|
dispatch_plugin_global_key(keysym, ev->keycode, CLEANMASK(ev->state), 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selmon->sel && !is_phone_locked()) {
|
||||||
|
if (dispatch_plugin_idle_key(keysym, ev->keycode, CLEANMASK(ev->state)))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!selmon->sel && CLEANMASK(ev->state) == 0) {
|
if (!selmon->sel && CLEANMASK(ev->state) == 0) {
|
||||||
if (keysym == XK_Return) {
|
if (keysym == XK_Return) {
|
||||||
const Arg entercmd = { .v = (const char*[]){ "sxmo_appmenu.sh", NULL } };
|
const Arg entercmd = { .v = (const char*[]){ "sxmo_appmenu.sh", NULL } };
|
||||||
@@ -2138,7 +2338,7 @@ run(void)
|
|||||||
int hw_down = query_softkeys_down();
|
int hw_down = query_softkeys_down();
|
||||||
softkey_pending |= hw_down;
|
softkey_pending |= hw_down;
|
||||||
if (softkey_pending == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
|
if (softkey_pending == (SOFTKEY_LEFT | SOFTKEY_RIGHT)) {
|
||||||
trigger_xdrawer_chord(now);
|
trigger_softkey_chord(now);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
long long rem = softkey_deadline_ms - now;
|
long long rem = softkey_deadline_ms - now;
|
||||||
@@ -2398,6 +2598,7 @@ setup(void)
|
|||||||
lidfd = open("/dev/input/by-path/platform-gpio-keys-event", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
|
lidfd = open("/dev/input/by-path/platform-gpio-keys-event", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
|
||||||
if (lidfd < 0)
|
if (lidfd < 0)
|
||||||
lidfd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
|
lidfd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
|
||||||
|
load_plugins();
|
||||||
grabkeys();
|
grabkeys();
|
||||||
focus(NULL);
|
focus(NULL);
|
||||||
}
|
}
|
||||||
@@ -2648,6 +2849,8 @@ unmapnotify(XEvent *e)
|
|||||||
setclientstate(c, WithdrawnState);
|
setclientstate(c, WithdrawnState);
|
||||||
else
|
else
|
||||||
unmanage(c, 0);
|
unmanage(c, 0);
|
||||||
|
} else if (selmon && !selmon->sel) {
|
||||||
|
focus(NULL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2658,7 +2861,7 @@ updatebars(void)
|
|||||||
XSetWindowAttributes wa = {
|
XSetWindowAttributes wa = {
|
||||||
.override_redirect = True,
|
.override_redirect = True,
|
||||||
.background_pixmap = ParentRelative,
|
.background_pixmap = ParentRelative,
|
||||||
.event_mask = ButtonPressMask|ExposureMask
|
.event_mask = ButtonPressMask|ExposureMask|KeyPressMask|KeyReleaseMask
|
||||||
};
|
};
|
||||||
XClassHint ch = {"dwm", "dwm"};
|
XClassHint ch = {"dwm", "dwm"};
|
||||||
for (m = mons; m; m = m->next) {
|
for (m = mons; m; m = m->next) {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#ifndef DWM_PLUGIN_H
|
||||||
|
#define DWM_PLUGIN_H
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define DWM_PLUGIN_API_VERSION 1
|
||||||
|
|
||||||
|
typedef struct DwmHostApi {
|
||||||
|
int api_version;
|
||||||
|
Display *dpy;
|
||||||
|
Window root;
|
||||||
|
int screen;
|
||||||
|
int sw;
|
||||||
|
int sh;
|
||||||
|
int bh;
|
||||||
|
int (*is_idle_desktop)(void);
|
||||||
|
int (*is_phone_locked)(void);
|
||||||
|
void (*spawn_command)(const char *const *argv);
|
||||||
|
void (*spawn_plugin)(const char *plugin_name, const char *arg);
|
||||||
|
} DwmHostApi;
|
||||||
|
|
||||||
|
typedef struct DwmPlugin {
|
||||||
|
int api_version;
|
||||||
|
const char *name;
|
||||||
|
const char *description;
|
||||||
|
int (*init)(const DwmHostApi *host);
|
||||||
|
void (*cleanup)(void);
|
||||||
|
int (*on_softkey_chord)(const DwmHostApi *host);
|
||||||
|
int (*on_idle_key)(const DwmHostApi *host, KeySym keysym, unsigned int keycode, unsigned int state);
|
||||||
|
int (*on_global_key)(const DwmHostApi *host, KeySym keysym, unsigned int keycode, unsigned int state, int is_press);
|
||||||
|
int (*run_main)(int argc, char **argv);
|
||||||
|
} DwmPlugin;
|
||||||
|
|
||||||
|
#define DWM_PLUGIN_ENTRY_SYMBOL "dwm_plugin_entry"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DWM_PLUGIN_H */
|
||||||
+32
-1
@@ -14,6 +14,8 @@
|
|||||||
#include <pango/pango.h>
|
#include <pango/pango.h>
|
||||||
#include <pango/pangoxft.h>
|
#include <pango/pangoxft.h>
|
||||||
|
|
||||||
|
#include "../sxmo-dwm/dwm_plugin.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
@@ -433,7 +435,8 @@ class PhoneController {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
static int xdrawer_run_main(int, char**) {
|
||||||
|
gRunning = 1;
|
||||||
int64_t startMs = nowMs();
|
int64_t startMs = nowMs();
|
||||||
std::string pidPath = getRuntimeFile("xdrawer.pid");
|
std::string pidPath = getRuntimeFile("xdrawer.pid");
|
||||||
std::string startPath = getRuntimeFile("xdrawer.start");
|
std::string startPath = getRuntimeFile("xdrawer.start");
|
||||||
@@ -870,3 +873,31 @@ int main() {
|
|||||||
::unlink(pidPath.c_str());
|
::unlink(pidPath.c_str());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int xdrawer_plugin_init(const DwmHostApi*) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int xdrawer_on_softkey_chord(const DwmHostApi* host) {
|
||||||
|
if (host && host->spawn_plugin) {
|
||||||
|
host->spawn_plugin("xdrawer", nullptr);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
||||||
|
DWM_PLUGIN_API_VERSION,
|
||||||
|
"xdrawer",
|
||||||
|
"Global phone hardware controls drawer (softkey chord)",
|
||||||
|
xdrawer_plugin_init,
|
||||||
|
nullptr,
|
||||||
|
xdrawer_on_softkey_chord,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
xdrawer_run_main
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
return xdrawer_run_main(argc, argv);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user