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

1494 lines
51 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0-only
// Bananarchy Networks & Bluetooth Manager Plugin (networks.so / xnetworks)
#include "phone_data.h"
#include "../sxmo-dwm/dwm_plugin.h"
#include <X11/Xatom.h>
#include <X11/Xft/Xft.h>
#include <X11/Xutil.h>
#include <pango/pangoxft.h>
#include <poll.h>
#include <sys/stat.h>
#include <clocale>
#ifdef None
#undef None
#endif
namespace {
using namespace bananarchy;
volatile sig_atomic_t gRunning = 1;
void handleSignal(int) {
gRunning = 0;
}
enum class NetTab {
Wifi = 0,
Cellular = 1,
Bluetooth = 2,
};
enum class NetModal {
None = 0,
WifiActions = 1,
WifiPassword = 2,
ApnEditor = 3,
BtActions = 4,
};
enum class TextMode {
Lower = 0, // abc
Title = 1, // Abc
Upper = 2, // ABC
Num = 3, // 123
Sym = 4, // Sym
};
const char* kMultiTapMap[10] = {
" 0+_-", // 0
".,?!@-_1:/#", // 1
"abc2", // 2
"def3", // 3
"ghi4", // 4
"jkl5", // 5
"mno6", // 6
"pqrs7", // 7
"tuv8", // 8
"wxyz9" // 9
};
const char* kSymTapMap[10] = {
" 0+=", // 0
".,?!@1", // 1
"_-/:;2", // 2
"#*&$%3", // 3
"()[]{}4", // 4
"<>\"'`5", // 5
"\\|^~6", // 6
"+-=*7", // 7
"$,.:8", // 8
"!?;:9" // 9
};
void popUtf8Char(std::string& s) {
if (s.empty()) return;
while (!s.empty() && (static_cast<unsigned char>(s.back()) & 0xC0) == 0x80) {
s.pop_back();
}
if (!s.empty()) s.pop_back();
}
struct WifiApEntry {
std::string ssid;
int signal = 0;
std::string security;
bool inUse = false;
bool isSaved = false;
};
struct CellularNetInfo {
bool gsmConfigured = false;
bool gsmActive = false;
std::string gsmConName;
std::string apn;
std::string username;
std::string ipType = "ipv4v6";
std::string wwanIp;
std::string wlanIp;
bool hotspotActive = false;
std::string hotspotInfo;
bool usbShareActive = false;
};
struct BtDeviceEntry {
std::string mac;
std::string name;
bool paired = false;
bool connected = false;
};
bool isWifiEnabledFast() {
std::string out = runCaptureCmd("/usr/sbin/rfkill list wifi 2>/dev/null");
if (out.find("Soft blocked: yes") != std::string::npos ||
out.find("Hard blocked: yes") != std::string::npos) {
return false;
}
std::string nm = trimStr(runCaptureCmd("nmcli radio wifi 2>/dev/null"));
if (nm == "disabled") return false;
return true;
}
bool isBtEnabledFast() {
std::string out = runCaptureCmd("/usr/sbin/rfkill list bluetooth 2>/dev/null");
if (out.find("Soft blocked: yes") != std::string::npos ||
out.find("Hard blocked: yes") != std::string::npos) {
return false;
}
return true;
}
std::vector<WifiApEntry> queryWifiList(bool forceRescan) {
std::vector<WifiApEntry> list;
std::unordered_set<std::string> savedSsids;
std::string conOut = runCaptureCmd(
"nmcli -t -f NAME,TYPE connection show 2>/dev/null");
{
std::istringstream ss(conOut);
std::string line;
while (std::getline(ss, line)) {
size_t col = line.rfind(':');
if (col == std::string::npos) continue;
std::string nm = line.substr(0, col);
std::string tp = line.substr(col + 1);
if (tp.find("wireless") != std::string::npos && !nm.empty()) {
savedSsids.insert(nm);
}
}
}
std::string cmd = std::string("nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan ") +
(forceRescan ? "yes" : "no") + " 2>/dev/null";
std::string out = runCaptureCmd(cmd);
std::istringstream ss(out);
std::string line;
std::unordered_map<std::string, size_t> seenIdx;
while (std::getline(ss, line)) {
if (line.empty()) continue;
// Parse IN-USE:SSID:SIGNAL:SECURITY (note SSID can contain escaped colons \:)
size_t c1 = line.find(':');
if (c1 == std::string::npos) continue;
size_t c3 = line.rfind(':');
if (c3 == std::string::npos || c3 <= c1) continue;
size_t c2 = line.rfind(':', c3 - 1);
if (c2 == std::string::npos || c2 <= c1) continue;
std::string inUseStr = line.substr(0, c1);
std::string rawSsid = line.substr(c1 + 1, c2 - c1 - 1);
std::string sigStr = line.substr(c2 + 1, c3 - c2 - 1);
std::string secStr = line.substr(c3 + 1);
std::string ssid;
for (size_t i = 0; i < rawSsid.size(); ++i) {
if (rawSsid[i] == '\\' && i + 1 < rawSsid.size()) {
ssid.push_back(rawSsid[++i]);
} else {
ssid.push_back(rawSsid[i]);
}
}
ssid = trimStr(ssid);
if (ssid.empty()) continue;
WifiApEntry ap;
ap.ssid = ssid;
ap.signal = std::clamp(std::atoi(sigStr.c_str()), 0, 100);
ap.security = (secStr == "--") ? "" : trimStr(secStr);
ap.inUse = (inUseStr == "*");
ap.isSaved = (savedSsids.count(ssid) > 0);
auto it = seenIdx.find(ssid);
if (it == seenIdx.end()) {
seenIdx[ssid] = list.size();
list.push_back(ap);
} else {
auto& existing = list[it->second];
if (ap.inUse) existing.inUse = true;
if (ap.signal > existing.signal) existing.signal = ap.signal;
if (existing.security.empty() && !ap.security.empty()) {
existing.security = ap.security;
}
}
}
std::sort(list.begin(), list.end(),
[](const WifiApEntry& a, const WifiApEntry& b) {
if (a.inUse != b.inUse) return a.inUse > b.inUse;
if (a.isSaved != b.isSaved) return a.isSaved > b.isSaved;
return a.signal > b.signal;
});
return list;
}
CellularNetInfo queryCellularInfo() {
CellularNetInfo info;
std::string allCons = runCaptureCmd(
"nmcli -t -f NAME,TYPE,ACTIVE connection show 2>/dev/null");
std::istringstream ss(allCons);
std::string line;
while (std::getline(ss, line)) {
size_t c2 = line.rfind(':');
if (c2 == std::string::npos) continue;
size_t c1 = line.rfind(':', c2 - 1);
if (c1 == std::string::npos) continue;
std::string nm = line.substr(0, c1);
std::string tp = line.substr(c1 + 1, c2 - c1 - 1);
std::string act = line.substr(c2 + 1);
if (tp == "gsm" && info.gsmConName.empty()) {
info.gsmConfigured = true;
info.gsmConName = nm;
info.gsmActive = (act == "yes");
}
if (nm == "Hotspot" && act == "yes") {
info.hotspotActive = true;
}
if (nm == "USBNetworkShare" && act == "yes") {
info.usbShareActive = true;
}
}
if (!info.usbShareActive) {
struct stat st {};
if (::stat("/tmp/bananarchy_usb_tether.active", &st) == 0) {
info.usbShareActive = true;
}
}
if (info.gsmConfigured && !info.gsmConName.empty()) {
std::string det = runCaptureCmd(
"nmcli -t -f gsm.apn,gsm.username connection show " +
shellQuote(info.gsmConName) + " 2>/dev/null");
std::istringstream ds(det);
std::string dline;
while (std::getline(ds, dline)) {
if (dline.rfind("gsm.apn:", 0) == 0) {
info.apn = trimStr(dline.substr(8));
if (info.apn == "--") info.apn.clear();
} else if (dline.rfind("gsm.username:", 0) == 0) {
info.username = trimStr(dline.substr(13));
if (info.username == "--") info.username.clear();
}
}
if (info.apn.empty()) info.apn = info.gsmConName;
}
std::string ipOut = runCaptureCmd("ip -4 -o addr show 2>/dev/null");
std::istringstream ips(ipOut);
while (std::getline(ips, line)) {
size_t inetPos = line.find("inet ");
if (inetPos == std::string::npos) continue;
std::string rest = line.substr(inetPos + 5);
size_t sp = rest.find(' ');
std::string cidr = (sp == std::string::npos) ? rest : rest.substr(0, sp);
if (line.find("wwan") != std::string::npos ||
line.find("rmnet") != std::string::npos) {
info.wwanIp = cidr;
} else if (line.find("wlan") != std::string::npos) {
info.wlanIp = cidr;
}
}
return info;
}
std::vector<BtDeviceEntry> queryBtDevices() {
std::vector<BtDeviceEntry> out;
std::unordered_set<std::string> pairedMacs;
std::unordered_set<std::string> connectedMacs;
auto parseMacSet = [](const std::string& raw,
std::unordered_set<std::string>& dst) {
std::istringstream ss(raw);
std::string line;
while (std::getline(ss, line)) {
// Format: "Device XX:XX:XX:XX:XX:XX Name..."
if (line.rfind("Device ", 0) == 0 && line.size() >= 24) {
dst.insert(line.substr(7, 17));
}
}
};
parseMacSet(runCaptureCmd("bluetoothctl devices Paired 2>/dev/null || "
"bluetoothctl paired-devices 2>/dev/null"),
pairedMacs);
parseMacSet(runCaptureCmd("bluetoothctl devices Connected 2>/dev/null"),
connectedMacs);
std::string allRaw = runCaptureCmd("bluetoothctl devices 2>/dev/null");
std::istringstream ss(allRaw);
std::string line;
while (std::getline(ss, line)) {
if (line.rfind("Device ", 0) != 0 || line.size() < 24) continue;
BtDeviceEntry d;
d.mac = line.substr(7, 17);
d.name = (line.size() > 25) ? trimStr(line.substr(25)) : d.mac;
d.paired = (pairedMacs.count(d.mac) > 0);
d.connected = (connectedMacs.count(d.mac) > 0);
out.push_back(d);
}
std::sort(out.begin(), out.end(),
[](const BtDeviceEntry& a, const BtDeviceEntry& b) {
if (a.connected != b.connected) return a.connected > b.connected;
if (a.paired != b.paired) return a.paired > b.paired;
return a.name < b.name;
});
return out;
}
int runNetworksApp(int argc, char** argv) {
gRunning = 1;
std::setlocale(LC_ALL, "");
XSetLocaleModifiers("");
bool isPreload = false;
std::string openArg;
const char* base0 = std::strrchr(argv[0], '/');
base0 = base0 ? (base0 + 1) : argv[0];
if (base0 && (strcasecmp(base0, "xbluetooth") == 0 ||
strcasecmp(base0, "bluetooth") == 0)) {
openArg = "bluetooth";
}
for (int i = 1; i < argc; ++i) {
if (!argv[i]) continue;
if (std::strcmp(argv[i], "--preload") == 0) {
isPreload = true;
} else if (argv[i][0] != '-') {
openArg = argv[i];
}
}
std::string daemonPidPath = getPluginDaemonPidPath("networks");
std::string activePidPath = getRuntimeFile("xnetworks.pid");
if (isPluginDaemonAlive("networks")) {
if (!isPreload) {
sendPluginCommand("networks",
openArg.empty() ? "OPEN" : ("OPEN " + openArg));
}
return 0;
}
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
int fifoFd = setupPluginDaemonFifo("networks");
struct sigaction sa {};
sa.sa_handler = handleSignal;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
Display* dpy = XOpenDisplay(nullptr);
if (!dpy) {
if (fifoFd >= 0) ::close(fifoFd);
::unlink(daemonPidPath.c_str());
return 1;
}
int screen = DefaultScreen(dpy);
Window root = RootWindow(dpy, screen);
Visual* visual = DefaultVisual(dpy, screen);
Colormap cmap = DefaultColormap(dpy, screen);
ThemeSpec initSpec;
initSpec.loadActive(dpy, screen);
XSetWindowAttributes swa {};
swa.override_redirect = True;
swa.background_pixel = initSpec.bgMain;
swa.border_pixel = initSpec.selBg;
swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask |
VisibilityChangeMask | StructureNotifyMask;
Window win = XCreateWindow(
dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0,
DefaultDepth(dpy, screen), CopyFromParent, visual,
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa);
XClassHint ch {};
char resName[] = "xnetworks";
char resClass[] = "XNetworks";
ch.res_name = resName;
ch.res_class = resClass;
XSetClassHint(dpy, win, &ch);
XStoreName(dpy, win, "Networks & Bluetooth");
Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight,
DefaultDepth(dpy, screen));
GC gc = XCreateGC(dpy, pix, 0, nullptr);
XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap);
PluginTheme pt;
pt.init(dpy, screen, visual, cmap, win);
auto& layoutMain = pt.layoutMain;
auto& layoutSmall = pt.layoutSmall;
auto& layoutIcon = pt.layoutIcon;
auto& colWhite = pt.colWhite;
auto& colLight = pt.colLight;
auto& colMuted = pt.colMuted;
auto& colYellow = pt.colYellow;
auto& colGreen = pt.colGreen;
auto& colBlue = pt.colBlue;
auto& colOrange = pt.colOrange;
NetTab activeTab = NetTab::Wifi;
NetModal modal = NetModal::None;
bool wifiOn = true;
std::vector<WifiApEntry> wifiList;
int wifiRowSel = 0; // 0 = Wi-Fi Toggle, 1..N = AP entries
CellularNetInfo cellInfo;
int cellRowSel = 0; // 0 = Mobile Data, 1 = Configure APN, 2 = Wi-Fi Hotspot, 3 = USB Share
bool btOn = false;
std::vector<BtDeviceEntry> btList;
int btRowSel = 0; // 0 = BT Power Toggle, 1 = Scan Devices, 2..N = BT devices
int actionSel = 0;
// Wi-Fi Password modal state
WifiApEntry targetAp;
std::string wifiPassword;
bool showPassword = false;
// APN Editor modal state
std::string apnName;
std::string apnUser;
std::string apnPass;
int apnFieldSel = 0; // 0 = APN, 1 = Username, 2 = Password, 3 = Save
// Bluetooth target device
BtDeviceEntry targetBt;
// Multi-tap state for text inputs
TextMode textMode = TextMode::Lower;
int lastTapDigit = -1;
int lastTapCycle = 0;
int64_t lastTapMs = 0;
std::string statusBanner;
int64_t statusBannerUntilMs = 0;
bool visible = false;
int64_t openStartMs = 0;
int64_t startupIgnoreUntilMs = 0;
bool initialSoftkeysReleased = true;
int softkeyPending = 0;
int64_t softkeyDeadlineMs = 0;
auto setBanner = [&](const std::string& msg, int durationMs = 2300) {
statusBanner = msg;
statusBannerUntilMs = nowMs() + durationMs;
};
auto refreshCurrentTab = [&](bool forceScan = false) {
if (activeTab == NetTab::Wifi) {
wifiOn = isWifiEnabledFast();
if (wifiOn) {
wifiList = queryWifiList(forceScan);
} else {
wifiList.clear();
}
int maxRow = static_cast<int>(wifiList.size());
if (wifiRowSel > maxRow) wifiRowSel = maxRow;
} else if (activeTab == NetTab::Cellular) {
cellInfo = queryCellularInfo();
} else if (activeTab == NetTab::Bluetooth) {
btOn = isBtEnabledFast();
if (btOn) {
if (forceScan) {
runCaptureCmd("bluetoothctl --timeout 5 scan on >/dev/null 2>&1");
}
btList = queryBtDevices();
} else {
btList.clear();
}
int maxRow = 1 + static_cast<int>(btList.size());
if (btRowSel > maxRow) btRowSel = maxRow;
}
};
auto applyOpenArg = [&](const std::string& arg) {
std::string a = toLowerStr(trimStr(arg));
modal = NetModal::None;
if (a == "bluetooth" || a == "bt") {
activeTab = NetTab::Bluetooth;
} else if (a == "cell" || a == "cellular" || a == "apn" || a == "hotspot") {
activeTab = NetTab::Cellular;
} else {
activeTab = NetTab::Wifi;
}
refreshCurrentTab(false);
};
auto textModeLabel = [&]() -> const char* {
switch (textMode) {
case TextMode::Lower: return "abc";
case TextMode::Title: return "Abc";
case TextMode::Upper: return "ABC";
case TextMode::Num: return "123";
case TextMode::Sym: return "Sym";
}
return "abc";
};
auto handleMultiTapKey = [&](std::string& targetStr, KeySym sym,
unsigned int kc, unsigned int state) -> bool {
char dch = mapDialKey(sym, kc, state);
if (dch == '#') {
textMode = static_cast<TextMode>((static_cast<int>(textMode) + 1) % 5);
lastTapDigit = -1;
return true;
}
if (dch >= '0' && dch <= '9') {
int digit = dch - '0';
int64_t now = nowMs();
if (textMode == TextMode::Num) {
targetStr.push_back(dch);
lastTapDigit = -1;
return true;
}
const char* seq = (textMode == TextMode::Sym)
? kSymTapMap[digit]
: kMultiTapMap[digit];
int seqLen = static_cast<int>(std::strlen(seq));
if (seqLen <= 0) return false;
if (digit == lastTapDigit && (now - lastTapMs) < 750 && !targetStr.empty()) {
popUtf8Char(targetStr);
lastTapCycle = (lastTapCycle + 1) % seqLen;
} else {
lastTapDigit = digit;
lastTapCycle = 0;
}
lastTapMs = now;
char outCh = seq[lastTapCycle];
if (textMode == TextMode::Upper) {
outCh = static_cast<char>(std::toupper(static_cast<unsigned char>(outCh)));
} else if (textMode == TextMode::Title && targetStr.empty()) {
outCh = static_cast<char>(std::toupper(static_cast<unsigned char>(outCh)));
}
targetStr.push_back(outCh);
return true;
}
if (sym == XK_BackSpace || sym == XK_Delete) {
if (!targetStr.empty()) {
popUtf8Char(targetStr);
lastTapDigit = -1;
return true;
}
}
// Also accept direct ASCII keysyms from XIM / hardware keyboards
if (sym >= 0x20 && sym <= 0x7e && !(state & (ControlMask | Mod1Mask))) {
targetStr.push_back(static_cast<char>(sym));
lastTapDigit = -1;
return true;
}
return false;
};
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h,
const std::string& text, int maxW = -1) -> int {
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);
}
int tw = 0, th = 0;
pango_layout_get_pixel_size(layout, &tw, &th);
int dy = y + std::max(0, (h - th) / 2);
pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE,
dy * PANGO_SCALE);
return tw;
};
auto pangoTextWidth = [&](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()));
int tw = 0, th = 0;
pango_layout_get_pixel_size(layout, &tw, &th);
return tw;
};
auto drawSoftkeyBar = [&](const std::string& leftLabel,
const std::string& centerLabel,
const std::string& rightLabel) {
const int barY = kWinHeight - 26;
XSetForeground(dpy, gc, pt.spec.bgFooter);
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26);
XSetForeground(dpy, gc, pt.spec.divider);
XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY);
drawPango(layoutMain, &colBlue, 6, barY, 26, leftLabel);
int cw = pangoTextWidth(layoutMain, centerLabel);
drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26, centerLabel);
int rw = pangoTextWidth(layoutMain, rightLabel);
drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel);
};
auto render = [&]() {
if (!visible) return;
pt.checkAndReload();
XSetForeground(dpy, gc, pt.spec.bgMain);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
// Top Tab Bar (Y=0..30)
XSetForeground(dpy, gc, pt.spec.bgHeader);
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 30);
XSetForeground(dpy, gc, pt.spec.border);
XDrawLine(dpy, pix, gc, 0, 29, kWinWidth, 29);
const char* tabLabels[3] = {"1:Wi-Fi", "2:Cell/APN", "3:Bluetooth"};
int tabW = kWinWidth / 3;
for (int t = 0; t < 3; ++t) {
bool isSel = (static_cast<int>(activeTab) == t);
if (isSel) {
XSetForeground(dpy, gc, pt.spec.selBg);
XFillRectangle(dpy, pix, gc, t * tabW, 0, tabW, 29);
XSetForeground(dpy, gc, pt.spec.accent);
XFillRectangle(dpy, pix, gc, t * tabW, 27, tabW, 2);
}
int tw = pangoTextWidth(layoutSmall, tabLabels[t]);
drawPango(layoutSmall, isSel ? &colWhite : &colMuted,
t * tabW + (tabW - tw) / 2, 0, 28, tabLabels[t]);
}
const int contentY = 32;
const int contentH = kWinHeight - 32 - 26;
if (activeTab == NetTab::Wifi) {
// Row 0: Wi-Fi Power Toggle (height 32)
bool sel0 = (wifiRowSel == 0);
XSetForeground(dpy, gc, sel0 ? pt.spec.selBg : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 30);
drawPango(layoutIcon, wifiOn ? &colGreen : &colMuted, 10, contentY, 30, kIconWifi);
drawPango(layoutMain, sel0 ? &colWhite : &colLight,
32, contentY, 30, std::string("Wi-Fi Radio: ") + (wifiOn ? "ON" : "OFF"));
drawPango(layoutIcon, wifiOn ? &colGreen : &colMuted,
kWinWidth - 28, contentY, 30, wifiOn ? kIconToggleOn : kIconToggleOff);
int listY = contentY + 34;
int listH = contentH - 34;
if (!wifiOn) {
drawPango(layoutMain, &colMuted, 18, listY + 50, 24,
"Wi-Fi is turned off.");
} else if (wifiList.empty()) {
drawPango(layoutMain, &colMuted, 18, listY + 50, 24,
"No networks found. Press Scan.");
} else {
const int rowH = 38;
const int maxVis = listH / rowH; // 5 rows
int apSel = std::max(0, wifiRowSel - 1);
int startIdx = std::clamp(apSel - maxVis / 2, 0,
std::max(0, static_cast<int>(wifiList.size()) - maxVis));
for (int i = 0; i < maxVis && (startIdx + i) < static_cast<int>(wifiList.size()); ++i) {
int idx = startIdx + i;
const auto& ap = wifiList[idx];
int ry = listY + i * rowH;
bool isSel = (wifiRowSel == (idx + 1));
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
: ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd));
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, rowH - 2);
drawPango(layoutIcon, ap.inUse ? &colGreen : (ap.isSaved ? &colBlue : &colLight),
10, ry + 1, 20, ap.security.empty() ? kIconWifi : kIconLock);
drawPango(layoutMain, isSel ? &colWhite : &colLight,
30, ry + 1, 20, ap.ssid, kWinWidth - 42);
std::string sub = std::to_string(ap.signal) + "% ";
if (ap.inUse) sub += "[Connected] ";
else if (ap.isSaved) sub += "[Saved] ";
sub += ap.security.empty() ? "Open" : ap.security;
drawPango(layoutSmall, ap.inUse ? &colGreen : (isSel ? &colLight : &colMuted),
30, ry + 19, 17, sub, kWinWidth - 42);
}
}
drawSoftkeyBar("Scan", "Select", "Close");
} else if (activeTab == NetTab::Cellular) {
// Status Card (height 86)
XSetForeground(dpy, gc, pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 86);
XSetForeground(dpy, gc, pt.spec.border);
XDrawRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 9, 85);
std::string cellSt = cellInfo.gsmActive ? "Connected"
: (cellInfo.gsmConfigured ? "Disconnected" : "Unconfigured");
drawPango(layoutMain, cellInfo.gsmActive ? &colGreen : &colOrange,
10, contentY + 2, 22, "Cellular: " + cellSt, 220);
drawPango(layoutSmall, &colLight, 10, contentY + 24, 18,
"APN: " + (cellInfo.apn.empty() ? "(not set)" : cellInfo.apn), 220);
drawPango(layoutSmall, &colMuted, 10, contentY + 43, 18,
"WWAN IP: " + (cellInfo.wwanIp.empty() ? "--" : cellInfo.wwanIp), 220);
drawPango(layoutSmall, &colMuted, 10, contentY + 62, 18,
"WLAN IP: " + (cellInfo.wlanIp.empty() ? "--" : cellInfo.wlanIp), 220);
// 4 Interactive Rows
std::string rows[4] = {
std::string(kIconGlobe) + " Mobile Data: " + (cellInfo.gsmActive ? "ON" : "OFF"),
std::string(kIconEdit) + " Configure APN (" + (cellInfo.apn.empty() ? "Set" : cellInfo.apn) + ")",
std::string(kIconWifi) + " Wi-Fi Hotspot: " + (cellInfo.hotspotActive ? "ON" : "OFF"),
std::string(kIconFlash) + " USB Tethering: " + (cellInfo.usbShareActive ? "ON" : "OFF"),
};
int baseY = contentY + 92;
for (int r = 0; r < 4; ++r) {
int ry = baseY + r * 32;
bool isSel = (cellRowSel == r);
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
: ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd));
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 30);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
10, ry, 30, rows[r], kWinWidth - 20);
}
drawSoftkeyBar("Refresh", "Select", "Close");
} else if (activeTab == NetTab::Bluetooth) {
// Row 0: BT Power Toggle (height 28)
bool sel0 = (btRowSel == 0);
XSetForeground(dpy, gc, sel0 ? pt.spec.selBg : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 28);
drawPango(layoutIcon, btOn ? &colBlue : &colMuted, 10, contentY, 28, kIconBluetooth);
drawPango(layoutMain, sel0 ? &colWhite : &colLight,
30, contentY, 28, std::string("Bluetooth: ") + (btOn ? "ON" : "OFF"));
drawPango(layoutIcon, btOn ? &colGreen : &colMuted,
kWinWidth - 28, contentY, 28, btOn ? kIconToggleOn : kIconToggleOff);
// Row 1: Scan for Devices (height 26)
bool sel1 = (btRowSel == 1);
XSetForeground(dpy, gc, sel1 ? pt.spec.selBg : pt.spec.bgAltOdd);
XFillRectangle(dpy, pix, gc, 4, contentY + 30, kWinWidth - 8, 26);
drawPango(layoutIcon, &colYellow, 10, contentY + 30, 26, kIconSearch);
drawPango(layoutSmall, sel1 ? &colWhite : &colLight,
30, contentY + 30, 26, "Scan for Bluetooth Devices");
int listY = contentY + 58;
int listH = contentH - 58;
if (!btOn) {
drawPango(layoutMain, &colMuted, 18, listY + 40, 24,
"Bluetooth is turned off.");
} else if (btList.empty()) {
drawPango(layoutMain, &colMuted, 18, listY + 40, 24,
"No devices. Select Scan.");
} else {
const int rowH = 38;
const int maxVis = listH / rowH; // 4 rows
int devSel = std::max(0, btRowSel - 2);
int startIdx = std::clamp(devSel - maxVis / 2, 0,
std::max(0, static_cast<int>(btList.size()) - maxVis));
for (int i = 0; i < maxVis && (startIdx + i) < static_cast<int>(btList.size()); ++i) {
int idx = startIdx + i;
const auto& d = btList[idx];
int ry = listY + i * rowH;
bool isSel = (btRowSel == (idx + 2));
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
: ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd));
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, rowH - 2);
drawPango(layoutIcon, d.connected ? &colGreen : (d.paired ? &colBlue : &colMuted),
10, ry + 1, 20, kIconBluetooth);
drawPango(layoutMain, isSel ? &colWhite : &colLight,
30, ry + 1, 20, d.name, kWinWidth - 42);
std::string st = d.connected ? "[Connected] "
: (d.paired ? "[Paired] " : "[Available] ");
st += d.mac;
drawPango(layoutSmall, d.connected ? &colGreen : (isSel ? &colLight : &colMuted),
30, ry + 19, 17, st, kWinWidth - 42);
}
}
drawSoftkeyBar("Scan", "Select", "Close");
}
// Modal Overlays
if (modal == NetModal::WifiActions) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 12, 48, kWinWidth - 24, 174);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 12, 48, kWinWidth - 25, 173);
drawPango(layoutMain, &colYellow, 20, 52, 24, targetAp.ssid, kWinWidth - 40);
const char* acts[4] = {
targetAp.inUse ? "Reconnect Network" : "Connect to Network",
"Enter / Update Password",
"Disconnect Network",
"Forget Saved Network",
};
for (int i = 0; i < 4; ++i) {
int ry = 82 + i * 32;
bool isSel = (actionSel == i);
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton);
XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 28);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
24, ry, 28, acts[i], kWinWidth - 48);
}
drawSoftkeyBar("Select", "OK", "Back");
} else if (modal == NetModal::WifiPassword) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 10, 42, kWinWidth - 20, 200);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 10, 42, kWinWidth - 21, 199);
drawPango(layoutMain, &colYellow, 18, 48, 22,
"Wi-Fi: " + targetAp.ssid, kWinWidth - 80);
drawPango(layoutSmall, &colBlue, kWinWidth - 58, 48, 22,
std::string("[") + textModeLabel() + "]");
drawPango(layoutSmall, &colLight, 18, 76, 20, "Enter WPA/WEP Passphrase:");
XSetForeground(dpy, gc, pt.spec.bgInputFocus);
XFillRectangle(dpy, pix, gc, 18, 100, kWinWidth - 36, 34);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 18, 100, kWinWidth - 37, 33);
std::string disp = showPassword ? wifiPassword
: std::string(wifiPassword.size(), '*');
drawPango(layoutMain, &colWhite, 24, 100, 34, disp + "_", kWinWidth - 48);
drawPango(layoutSmall, &colMuted, 18, 144, 18,
"[#] Cycle input mode (abc/Abc/ABC/123/Sym)");
drawPango(layoutSmall, &colMuted, 18, 164, 18,
"[*] Toggle Show/Hide password");
drawPango(layoutSmall, &colMuted, 18, 184, 18,
"[BackSpace] Delete character");
drawSoftkeyBar("Connect", "Connect", "Cancel");
} else if (modal == NetModal::ApnEditor) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 10, 36, kWinWidth - 20, 220);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 10, 36, kWinWidth - 21, 219);
drawPango(layoutMain, &colYellow, 18, 40, 22, "Cellular APN Setup");
drawPango(layoutSmall, &colBlue, kWinWidth - 58, 40, 22,
std::string("[") + textModeLabel() + "]");
const char* labels[3] = {"APN Name:", "Username (optional):", "Password (optional):"};
std::string vals[3] = {apnName, apnUser, apnPass};
for (int f = 0; f < 3; ++f) {
int fy = 66 + f * 44;
bool isF = (apnFieldSel == f);
drawPango(layoutSmall, isF ? &colYellow : &colMuted, 18, fy, 16, labels[f]);
XSetForeground(dpy, gc, isF ? pt.spec.bgInputFocus : pt.spec.bgInput);
XFillRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 36, 24);
XSetForeground(dpy, gc, isF ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 18, fy + 16, kWinWidth - 37, 23);
drawPango(layoutSmall, &colWhite, 22, fy + 16, 24,
vals[f] + (isF ? "_" : ""), kWinWidth - 44);
}
bool saveSel = (apnFieldSel == 3);
XSetForeground(dpy, gc, saveSel ? pt.spec.selBg : pt.spec.bgButton);
XFillRectangle(dpy, pix, gc, 18, 202, kWinWidth - 36, 26);
drawPango(layoutMain, saveSel ? &colWhite : &colGreen,
50, 202, 26, "Save & Activate APN");
drawSoftkeyBar("Save", "Select", "Cancel");
} else if (modal == NetModal::BtActions) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 12, 48, kWinWidth - 24, 174);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 12, 48, kWinWidth - 25, 173);
drawPango(layoutMain, &colYellow, 20, 52, 24, targetBt.name, kWinWidth - 40);
const char* acts[4] = {
"Connect Device",
"Disconnect Device",
"Pair & Trust Device",
"Forget / Remove Device",
};
for (int i = 0; i < 4; ++i) {
int ry = 82 + i * 32;
bool isSel = (actionSel == i);
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton);
XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 28);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
24, ry, 28, acts[i], kWinWidth - 48);
}
drawSoftkeyBar("Select", "OK", "Back");
}
if (!statusBanner.empty() && nowMs() < statusBannerUntilMs) {
XSetForeground(dpy, gc, pt.spec.bgToast);
XFillRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 16, 26);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 56, kWinWidth - 17, 25);
drawPango(layoutSmall, &colWhite, 14, kWinHeight - 56, 26,
statusBanner, kWinWidth - 28);
}
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
XFlush(dpy);
};
auto grabKeys = [&]() {
for (int i = 0; i < 6; ++i) {
if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync,
CurrentTime) == GrabSuccess) {
break;
}
usleep(15000);
}
};
auto showWindow = [&](const std::string& arg = "") {
closeOtherPlugins("networks");
applyOpenArg(arg);
writeIntFile(activePidPath, static_cast<int>(::getpid()));
if (!visible) {
XMapRaised(dpy, win);
visible = true;
} else {
XRaiseWindow(dpy, win);
}
openStartMs = nowMs();
startupIgnoreUntilMs = openStartMs + 180;
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
softkeyPending = 0;
grabKeys();
render();
};
auto hideWindow = [&]() {
if (!visible) return;
XUngrabKeyboard(dpy, CurrentTime);
XUnmapWindow(dpy, win);
XFlush(dpy);
visible = false;
modal = NetModal::None;
::unlink(activePidPath.c_str());
};
auto connectWifiNow = [&](const std::string& ssid, const std::string& pass) {
modal = NetModal::None;
setBanner("Connecting to " + ssid + "...");
render();
int rc = 0;
std::string cmd = "nmcli device wifi connect " + shellQuote(ssid);
if (!pass.empty()) {
cmd += " password " + shellQuote(pass);
}
cmd += " >/dev/null 2>&1";
runCaptureCmd(cmd, &rc);
refreshCurrentTab(false);
runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1");
setBanner(rc == 0 ? ("Connected to " + ssid) : ("Failed to connect to " + ssid));
};
auto saveAndConnectApn = [&]() {
std::string cleanApn = trimStr(apnName);
if (cleanApn.empty()) {
setBanner("APN name cannot be empty");
return;
}
modal = NetModal::None;
setBanner("Saving APN " + cleanApn + "...");
render();
if (cellInfo.gsmConfigured && !cellInfo.gsmConName.empty()) {
runCaptureCmd("nmcli connection delete " +
shellQuote(cellInfo.gsmConName) + " >/dev/null 2>&1");
}
std::string cmd = "nmcli connection add type gsm con-name " +
shellQuote(cleanApn) + " ifname '*' apn " +
shellQuote(cleanApn) + " connection.autoconnect yes";
if (!trimStr(apnUser).empty()) {
cmd += " gsm.username " + shellQuote(trimStr(apnUser));
}
if (!trimStr(apnPass).empty()) {
cmd += " gsm.password " + shellQuote(trimStr(apnPass));
}
cmd += " >/dev/null 2>&1 && nmcli connection up " +
shellQuote(cleanApn) + " >/dev/null 2>&1";
int rc = 0;
runCaptureCmd(cmd, &rc);
refreshCurrentTab(false);
runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1");
setBanner(rc == 0 ? ("Connected APN: " + cleanApn) : ("Saved APN: " + cleanApn));
};
auto executeWifiAction = [&](int act) {
modal = NetModal::None;
if (act == 0) {
if (!targetAp.security.empty() && !targetAp.isSaved && !targetAp.inUse) {
wifiPassword.clear();
showPassword = false;
modal = NetModal::WifiPassword;
} else {
setBanner("Connecting to " + targetAp.ssid + "...");
render();
int rc = 0;
if (targetAp.isSaved) {
runCaptureCmd("nmcli connection up " + shellQuote(targetAp.ssid) +
" >/dev/null 2>&1", &rc);
} else {
runCaptureCmd("nmcli device wifi connect " + shellQuote(targetAp.ssid) +
" >/dev/null 2>&1", &rc);
}
refreshCurrentTab(false);
runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1");
setBanner(rc == 0 ? ("Connected: " + targetAp.ssid) : "Connection failed");
}
} else if (act == 1) {
wifiPassword.clear();
showPassword = false;
modal = NetModal::WifiPassword;
} else if (act == 2) {
runCaptureCmd("nmcli connection down " + shellQuote(targetAp.ssid) +
" >/dev/null 2>&1");
refreshCurrentTab(false);
runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1");
setBanner("Disconnected " + targetAp.ssid);
} else if (act == 3) {
runCaptureCmd("nmcli connection delete " + shellQuote(targetAp.ssid) +
" >/dev/null 2>&1");
refreshCurrentTab(false);
runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1");
setBanner("Forgot " + targetAp.ssid);
}
};
auto executeBtAction = [&](int act) {
modal = NetModal::None;
int rc = 0;
if (act == 0) {
setBanner("Connecting " + targetBt.name + "...");
render();
runCaptureCmd("bluetoothctl connect " + shellQuote(targetBt.mac) +
" >/dev/null 2>&1", &rc);
refreshCurrentTab(false);
setBanner(rc == 0 ? ("Connected: " + targetBt.name) : "Connect failed");
} else if (act == 1) {
setBanner("Disconnecting " + targetBt.name + "...");
render();
runCaptureCmd("bluetoothctl disconnect " + shellQuote(targetBt.mac) +
" >/dev/null 2>&1", &rc);
refreshCurrentTab(false);
setBanner("Disconnected " + targetBt.name);
} else if (act == 2) {
setBanner("Pairing " + targetBt.name + "...");
render();
runCaptureCmd("bluetoothctl --timeout 8 pair " + shellQuote(targetBt.mac) +
" >/dev/null 2>&1; bluetoothctl trust " +
shellQuote(targetBt.mac) + " >/dev/null 2>&1; "
"bluetoothctl connect " + shellQuote(targetBt.mac) +
" >/dev/null 2>&1", &rc);
refreshCurrentTab(false);
setBanner("Paired & trusted " + targetBt.name);
} else if (act == 3) {
runCaptureCmd("bluetoothctl remove " + shellQuote(targetBt.mac) +
" >/dev/null 2>&1", &rc);
refreshCurrentTab(false);
setBanner("Removed " + targetBt.name);
}
};
auto handleEnterKey = [&]() {
if (modal == NetModal::WifiActions) {
executeWifiAction(actionSel);
return;
}
if (modal == NetModal::WifiPassword) {
connectWifiNow(targetAp.ssid, wifiPassword);
return;
}
if (modal == NetModal::ApnEditor) {
if (apnFieldSel == 3) {
saveAndConnectApn();
} else {
apnFieldSel = (apnFieldSel + 1) % 4;
}
return;
}
if (modal == NetModal::BtActions) {
executeBtAction(actionSel);
return;
}
if (activeTab == NetTab::Wifi) {
if (wifiRowSel == 0) {
setBanner(wifiOn ? "Turning Wi-Fi OFF..." : "Turning Wi-Fi ON...");
render();
runCaptureCmd("doas -n sxmo_wifitoggle.sh " +
std::string(wifiOn ? "off" : "on") + " >/dev/null 2>&1");
refreshCurrentTab(false);
runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1");
} else if (wifiRowSel - 1 < static_cast<int>(wifiList.size())) {
targetAp = wifiList[wifiRowSel - 1];
if (!targetAp.inUse && !targetAp.isSaved && !targetAp.security.empty()) {
wifiPassword.clear();
showPassword = false;
modal = NetModal::WifiPassword;
} else {
actionSel = 0;
modal = NetModal::WifiActions;
}
}
} else if (activeTab == NetTab::Cellular) {
if (cellRowSel == 0) {
if (!cellInfo.gsmConfigured) {
apnName = cellInfo.apn;
apnUser = cellInfo.username;
apnPass.clear();
apnFieldSel = 0;
modal = NetModal::ApnEditor;
} else {
bool turnOn = !cellInfo.gsmActive;
setBanner(turnOn ? "Enabling Mobile Data..." : "Disabling Mobile Data...");
render();
runCaptureCmd("nmcli connection " + std::string(turnOn ? "up " : "down ") +
shellQuote(cellInfo.gsmConName) + " >/dev/null 2>&1");
refreshCurrentTab(false);
}
} else if (cellRowSel == 1) {
apnName = cellInfo.apn;
apnUser = cellInfo.username;
apnPass.clear();
apnFieldSel = 0;
modal = NetModal::ApnEditor;
} else if (cellRowSel == 2) {
if (cellInfo.hotspotActive) {
setBanner("Stopping Wi-Fi Hotspot...");
render();
runCaptureCmd("nmcli connection delete Hotspot >/dev/null 2>&1");
refreshCurrentTab(false);
setBanner("Hotspot stopped");
} else {
setBanner("Starting Wi-Fi Hotspot...");
render();
std::string hsOut = trimStr(runCaptureCmd(
"nmcli device wifi hotspot 2>&1"));
refreshCurrentTab(false);
setBanner(hsOut.empty() ? "Hotspot active" : hsOut, 4000);
}
runAsyncCmd("sxmo_hook_statusbar.sh wifi >/dev/null 2>&1");
} else if (cellRowSel == 3) {
if (cellInfo.usbShareActive) {
setBanner("Stopping USB Tethering...");
render();
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection usb-tether off >/dev/null 2>&1");
refreshCurrentTab(false);
setBanner("USB Tethering stopped");
} else {
setBanner("Starting USB Tethering...");
render();
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection usb-tether on >/dev/null 2>&1");
refreshCurrentTab(false);
setBanner("USB Tethering active");
}
}
} else if (activeTab == NetTab::Bluetooth) {
if (btRowSel == 0) {
setBanner(btOn ? "Turning Bluetooth OFF..." : "Turning Bluetooth ON...");
render();
runCaptureCmd("SXMO_OS=alpine doas -n sxmo_bluetoothtoggle.sh " +
std::string(btOn ? "off" : "on") + " >/dev/null 2>&1");
refreshCurrentTab(false);
} else if (btRowSel == 1) {
setBanner("Scanning Bluetooth devices (5s)...");
render();
refreshCurrentTab(true);
setBanner("Found " + std::to_string(btList.size()) + " device(s)");
} else if (btRowSel - 2 < static_cast<int>(btList.size())) {
targetBt = btList[btRowSel - 2];
actionSel = 0;
modal = NetModal::BtActions;
}
}
};
auto handleLeftSoftkey = [&]() {
if (modal == NetModal::ApnEditor) {
saveAndConnectApn();
return;
}
if (modal != NetModal::None) {
handleEnterKey();
return;
}
if (activeTab == NetTab::Wifi) {
setBanner("Scanning Wi-Fi networks...");
render();
refreshCurrentTab(true);
setBanner("Found " + std::to_string(wifiList.size()) + " network(s)");
} else if (activeTab == NetTab::Cellular) {
refreshCurrentTab(false);
setBanner("Refreshed cellular state");
} else if (activeTab == NetTab::Bluetooth) {
setBanner("Scanning Bluetooth (5s)...");
render();
refreshCurrentTab(true);
setBanner("Found " + std::to_string(btList.size()) + " device(s)");
}
};
auto handleRightSoftkey = [&]() {
if (modal != NetModal::None) {
modal = NetModal::None;
return;
}
hideWindow();
};
if (!isPreload) {
showWindow(openArg);
}
int xfd = ConnectionNumber(dpy);
while (gRunning) {
while (XPending(dpy) > 0) {
XEvent ev;
XNextEvent(dpy, &ev);
if (ev.type == Expose && ev.xexpose.count == 0) {
render();
} else if (ev.type == VisibilityNotify && visible &&
ev.xvisibility.state != VisibilityUnobscured) {
XRaiseWindow(dpy, win);
} else if (ev.type == KeyRelease) {
if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
initialSoftkeysReleased = true;
}
} else if (ev.type == KeyPress && visible) {
int64_t now = nowMs();
if (now < startupIgnoreUntilMs) continue;
KeySym sym = XLookupKeysym(&ev.xkey, 0);
unsigned int kc = ev.xkey.keycode;
bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu ||
sym == XF86XK_TaskPane);
bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back ||
sym == XK_Escape ||
sym == static_cast<KeySym>(XF86XK_KbdLcdMenu2));
if (isLSK || isRSK) {
if (!initialSoftkeysReleased) continue;
int mask = querySoftkeysDown(dpy);
if (mask == 3 || (softkeyPending != 0 &&
((isLSK && softkeyPending == 2) ||
(isRSK && softkeyPending == 1)))) {
softkeyPending = 0;
writeSoftkeySuppress(now + 400);
hideWindow();
sendPluginCommand("xdrawer", "TOGGLE");
continue;
}
softkeyPending = isLSK ? 1 : 2;
softkeyDeadlineMs = now + 90;
continue;
}
if (sym == XF86XK_HangupPhone || sym == XK_Cancel) {
hideWindow();
continue;
}
if (modal == NetModal::WifiPassword) {
char dch = mapDialKey(sym, kc, ev.xkey.state);
if (dch == '*') {
showPassword = !showPassword;
render();
continue;
}
if (sym == XK_Return || sym == XK_KP_Enter) {
connectWifiNow(targetAp.ssid, wifiPassword);
render();
continue;
}
if (handleMultiTapKey(wifiPassword, sym, kc, ev.xkey.state)) {
render();
}
continue;
}
if (modal == NetModal::ApnEditor) {
if (sym == XK_Up || sym == XK_KP_Up) {
apnFieldSel = (apnFieldSel + 3) % 4;
lastTapDigit = -1;
render();
continue;
}
if (sym == XK_Down || sym == XK_KP_Down) {
apnFieldSel = (apnFieldSel + 1) % 4;
lastTapDigit = -1;
render();
continue;
}
if (sym == XK_Return || sym == XK_KP_Enter) {
handleEnterKey();
render();
continue;
}
if (apnFieldSel < 3) {
std::string* dst = (apnFieldSel == 0) ? &apnName
: ((apnFieldSel == 1) ? &apnUser : &apnPass);
if (handleMultiTapKey(*dst, sym, kc, ev.xkey.state)) {
render();
}
}
continue;
}
if (modal == NetModal::WifiActions || modal == NetModal::BtActions) {
if (sym == XK_Up || sym == XK_KP_Up) {
actionSel = (actionSel + 3) % 4;
render();
} else if (sym == XK_Down || sym == XK_KP_Down) {
actionSel = (actionSel + 1) % 4;
render();
} else if (sym == XK_Return || sym == XK_KP_Enter) {
handleEnterKey();
render();
}
continue;
}
// Main View Tab & Row Navigation
char dch = mapDialKey(sym, kc, ev.xkey.state);
if (dch == '1') {
activeTab = NetTab::Wifi;
refreshCurrentTab(false);
render();
continue;
} else if (dch == '2') {
activeTab = NetTab::Cellular;
refreshCurrentTab(false);
render();
continue;
} else if (dch == '3') {
activeTab = NetTab::Bluetooth;
refreshCurrentTab(false);
render();
continue;
}
if (sym == XK_Left || sym == XK_KP_Left) {
activeTab = static_cast<NetTab>((static_cast<int>(activeTab) + 2) % 3);
refreshCurrentTab(false);
render();
} else if (sym == XK_Right || sym == XK_KP_Right) {
activeTab = static_cast<NetTab>((static_cast<int>(activeTab) + 1) % 3);
refreshCurrentTab(false);
render();
} else if (sym == XK_Up || sym == XK_KP_Up) {
if (activeTab == NetTab::Wifi) {
int total = 1 + static_cast<int>(wifiList.size());
wifiRowSel = (wifiRowSel + total - 1) % total;
} else if (activeTab == NetTab::Cellular) {
cellRowSel = (cellRowSel + 3) % 4;
} else if (activeTab == NetTab::Bluetooth) {
int total = 2 + static_cast<int>(btList.size());
btRowSel = (btRowSel + total - 1) % total;
}
render();
} else if (sym == XK_Down || sym == XK_KP_Down) {
if (activeTab == NetTab::Wifi) {
int total = 1 + static_cast<int>(wifiList.size());
wifiRowSel = (wifiRowSel + 1) % total;
} else if (activeTab == NetTab::Cellular) {
cellRowSel = (cellRowSel + 1) % 4;
} else if (activeTab == NetTab::Bluetooth) {
int total = 2 + static_cast<int>(btList.size());
btRowSel = (btRowSel + 1) % total;
}
render();
} else if (sym == XK_Return || sym == XK_KP_Enter) {
handleEnterKey();
render();
}
}
}
int timeoutMs = -1;
int64_t now = nowMs();
if (visible) {
if (softkeyPending != 0) {
int64_t rem = softkeyDeadlineMs - now;
if (rem <= 0) {
int pend = softkeyPending;
softkeyPending = 0;
if (querySoftkeysDown(dpy) == 3) {
writeSoftkeySuppress(now + 400);
hideWindow();
sendPluginCommand("xdrawer", "TOGGLE");
} else if (pend == 1) {
handleLeftSoftkey();
render();
} else if (pend == 2) {
handleRightSoftkey();
render();
}
} else {
timeoutMs = static_cast<int>(rem);
}
}
if (!statusBanner.empty()) {
int64_t bRem = statusBannerUntilMs - now;
if (bRem <= 0) {
statusBanner.clear();
render();
} else if (timeoutMs < 0 || bRem < timeoutMs) {
timeoutMs = static_cast<int>(bRem);
}
}
}
struct pollfd pfds[2];
pfds[0].fd = xfd;
pfds[0].events = POLLIN;
pfds[1].fd = fifoFd;
pfds[1].events = POLLIN;
int pr = ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs);
if (pr > 0 && fifoFd >= 0 && (pfds[1].revents & POLLIN)) {
for (const auto& cmd : readFifoLines(fifoFd)) {
if (cmd == "CLOSE") {
hideWindow();
} else if (cmd == "RELOAD_THEME") {
pt.reload();
if (visible) render();
} else if (cmd == "TOGGLE") {
if (visible) hideWindow();
else showWindow();
} else if (cmd.rfind("OPEN", 0) == 0) {
std::string arg = (cmd.size() > 5) ? trimStr(cmd.substr(5)) : "";
showWindow(arg);
}
}
}
}
hideWindow();
if (fifoFd >= 0) ::close(fifoFd);
::unlink(daemonPidPath.c_str());
XftDrawDestroy(xftDraw);
XFreePixmap(dpy, pix);
XFreeGC(dpy, gc);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
return 0;
}
int pluginInit(const DwmHostApi* host) {
if (host && host->spawn_plugin) {
host->spawn_plugin("networks", "--preload");
}
return 0;
}
void pluginCleanup(void) {}
} // namespace
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
DWM_PLUGIN_API_VERSION,
"networks",
"Bananarchy Wi-Fi, Cellular APN, Hotspot & Bluetooth Manager (240x320)",
pluginInit,
pluginCleanup,
nullptr,
nullptr,
nullptr,
runNetworksApp
};