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

1904 lines
67 KiB
C++

// SPDX-License-Identifier: GPL-2.0-only
// Bananarchy Modem, Dual-SIM & PIN/PUK Manager Plugin (modem.so / xmodem)
#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 TabId {
SimCards = 0,
RadioInfo = 1,
AtConsole = 2,
ModemLog = 3,
};
enum class ModalMode {
None = 0,
SimActions = 1,
VerifyPin = 2,
UnblockPuk = 3,
TogglePinLock = 4,
ChangePin = 5,
UssdDialog = 6,
};
struct SimSlotInfo {
int slotNum = 1;
std::string state = "absent"; // present, absent, error
std::string rawState = "absent";
std::string appType = "none"; // usim, sim, none
std::string appState = "none";
std::string aid;
std::string pin1State = "not-initialized";
int pin1Retries = 0;
int puk1Retries = 0;
std::string upinState = "not-initialized";
int upinRetries = 0;
int upukRetries = 0;
bool isActiveProvisioned = false;
bool isPreferred = false;
bool needsPin() const {
return state == "present" &&
(pin1State == "enabled-not-verified" || appState == "pin1");
}
bool needsPuk() const {
return state == "present" &&
(pin1State == "blocked" || appState == "puk1");
}
bool isPinEnabled() const {
return pin1State == "enabled-not-verified" ||
pin1State == "enabled-verified" ||
pin1State == "blocked";
}
};
struct ModemStatusData {
bool qmiAvailable = false;
int preferredSlot = 1;
int primaryGwSlot = 0;
SimSlotInfo slots[2];
bool mmPresent = false;
std::string mmState = "nomodem";
std::string mmFailedReason;
std::string mmPowerState = "off";
std::string mmUnlockRequired;
int mmPinRetries = -1;
int mmPukRetries = -1;
std::string operatorName;
std::string operatorCode;
std::string accessTech;
int signalQuality = 0;
std::string imei;
std::string imei2;
std::string imeiSv;
std::string ownNumber;
std::string simDbusPath;
std::string simIccid;
std::string simOperatorName;
std::string currentModes;
int modePresetIdx = 0;
};
struct RadioModePreset {
const char* label;
const char* allowed;
const char* preferred;
};
constexpr RadioModePreset kRadioModes[] = {
{"2G/3G/4G (4G Pref)", "2g|3g|4g", "4g"},
{"2G/3G/4G (3G Pref)", "2g|3g|4g", "3g"},
{"2G/3G (3G Pref)", "2g|3g", "3g"},
{"4G LTE Only", "4g", "none"},
{"3G UMTS Only", "3g", "none"},
{"2G GSM Only", "2g", "none"},
};
constexpr int kNumRadioModes =
static_cast<int>(sizeof(kRadioModes) / sizeof(kRadioModes[0]));
struct ModemLogRow {
std::string timeStr;
std::string eventType;
std::string target;
std::string detail;
};
constexpr const char* kAtPresets[] = {
"AT",
"ATI",
"AT+CGSN",
"AT+CSQ",
"AT+CREG?",
"AT+COPS?",
"AT+CPIN?",
"AT+CCID",
"AT+CGDCONT?",
"AT+CGPADDR",
"AT+CFUN?",
"AT+CLCC",
};
constexpr int kNumAtPresets =
static_cast<int>(sizeof(kAtPresets) / sizeof(kAtPresets[0]));
enum class AtTextMode {
Upper = 0, // ABC
Lower = 1, // abc
Num = 2, // 123
Sym = 3, // Sym
};
const char* kAtTapMap[10] = {
" 0+=",
"+?=,1\"':;/-_",
"ABC2",
"DEF3",
"GHI4",
"JKL5",
"MNO6",
"PQRS7",
"TUV8",
"WXYZ9"
};
void parseKvLines(const std::string& text,
std::unordered_map<std::string, std::string>& out,
char sep) {
std::istringstream ss(text);
std::string line;
while (std::getline(ss, line)) {
size_t pos = line.find(sep);
if (pos == std::string::npos) continue;
std::string k = trimStr(line.substr(0, pos));
std::string v = trimStr(line.substr(pos + 1));
if (!k.empty()) {
out[k] = v;
}
}
}
ModemStatusData queryModemAndSimStatus() {
ModemStatusData d;
d.slots[0].slotNum = 1;
d.slots[1].slotNum = 2;
// 1. Query QMI UIM status for both physical slots via helper
std::string uimOut = runCaptureCmd(
"doas -n /usr/libexec/msm-modem-uim-selection status 2>/dev/null || "
"/usr/libexec/msm-modem-uim-selection status 2>/dev/null");
std::unordered_map<std::string, std::string> uimKv;
parseKvLines(uimOut, uimKv, '=');
d.qmiAvailable = (uimKv["MODEM_AVAILABLE"] == "1");
if (!uimKv["PREFERRED_SLOT"].empty()) {
d.preferredSlot = std::clamp(std::atoi(uimKv["PREFERRED_SLOT"].c_str()), 1, 2);
}
if (!uimKv["PRIMARY_GW_SLOT"].empty()) {
d.primaryGwSlot = std::atoi(uimKv["PRIMARY_GW_SLOT"].c_str());
}
for (int i = 0; i < 2; ++i) {
std::string pfx = "SLOT" + std::to_string(i + 1) + "_";
auto& s = d.slots[i];
s.slotNum = i + 1;
if (!uimKv[pfx + "STATE"].empty()) s.state = uimKv[pfx + "STATE"];
if (!uimKv[pfx + "RAW_STATE"].empty()) s.rawState = uimKv[pfx + "RAW_STATE"];
if (!uimKv[pfx + "APP_TYPE"].empty()) s.appType = uimKv[pfx + "APP_TYPE"];
if (!uimKv[pfx + "APP_STATE"].empty()) s.appState = uimKv[pfx + "APP_STATE"];
s.aid = uimKv[pfx + "AID"];
if (!uimKv[pfx + "PIN1_STATE"].empty()) s.pin1State = uimKv[pfx + "PIN1_STATE"];
if (!uimKv[pfx + "PIN1_RETRIES"].empty()) {
s.pin1Retries = std::atoi(uimKv[pfx + "PIN1_RETRIES"].c_str());
}
if (!uimKv[pfx + "PUK1_RETRIES"].empty()) {
s.puk1Retries = std::atoi(uimKv[pfx + "PUK1_RETRIES"].c_str());
}
if (!uimKv[pfx + "UPIN_STATE"].empty()) s.upinState = uimKv[pfx + "UPIN_STATE"];
if (!uimKv[pfx + "UPIN_RETRIES"].empty()) {
s.upinRetries = std::atoi(uimKv[pfx + "UPIN_RETRIES"].c_str());
}
if (!uimKv[pfx + "UPUK_RETRIES"].empty()) {
s.upukRetries = std::atoi(uimKv[pfx + "UPUK_RETRIES"].c_str());
}
s.isActiveProvisioned = (d.primaryGwSlot == (i + 1));
s.isPreferred = (d.preferredSlot == (i + 1));
}
// 2. Query ModemManager (-m any -K)
std::string mmOut = runCaptureCmd("mmcli -m any -K 2>/dev/null");
std::unordered_map<std::string, std::string> mmKv;
parseKvLines(mmOut, mmKv, ':');
auto cleanMmVal = [](const std::string& v) -> std::string {
return (v == "--") ? "" : v;
};
if (!mmKv["modem.dbus-path"].empty()) {
d.mmPresent = true;
d.mmState = cleanMmVal(mmKv["modem.generic.state"]);
d.mmFailedReason = cleanMmVal(mmKv["modem.generic.state-failed-reason"]);
d.mmPowerState = cleanMmVal(mmKv["modem.generic.power-state"]);
d.mmUnlockRequired = cleanMmVal(mmKv["modem.generic.unlock-required"]);
d.operatorName = cleanMmVal(mmKv["modem.3gpp.operator-name"]);
d.operatorCode = cleanMmVal(mmKv["modem.3gpp.operator-code"]);
d.accessTech = cleanMmVal(mmKv["modem.generic.access-technologies.value[1]"]);
if (d.accessTech.empty()) {
d.accessTech = cleanMmVal(mmKv["modem.generic.access-technologies"]);
}
std::string sigStr = cleanMmVal(mmKv["modem.generic.signal-quality.value"]);
if (!sigStr.empty()) {
d.signalQuality = std::clamp(std::atoi(sigStr.c_str()), 0, 100);
}
d.imei = cleanMmVal(mmKv["modem.3gpp.imei"]);
if (d.imei.empty()) {
d.imei = cleanMmVal(mmKv["modem.generic.equipment-identifier"]);
}
DeviceImeis devImeis = queryDeviceImeis();
if (!devImeis.imei1.empty()) d.imei = devImeis.imei1;
if (!devImeis.imei2.empty()) d.imei2 = devImeis.imei2;
if (!devImeis.imeiSv.empty()) d.imeiSv = devImeis.imeiSv;
if (d.imei2.empty()) d.imei2 = d.imei;
d.ownNumber = cleanMmVal(mmKv["modem.generic.own-numbers.value[1]"]);
d.simDbusPath = cleanMmVal(mmKv["modem.generic.sim"]);
d.currentModes = cleanMmVal(mmKv["modem.generic.current-modes"]);
if (d.currentModes.find("allowed: 2g, 3g, 4g; preferred: 4g") != std::string::npos) {
d.modePresetIdx = 0;
} else if (d.currentModes.find("allowed: 2g, 3g, 4g; preferred: 3g") != std::string::npos) {
d.modePresetIdx = 1;
} else if (d.currentModes.find("allowed: 2g, 3g; preferred: 3g") != std::string::npos) {
d.modePresetIdx = 2;
} else if (d.currentModes.find("allowed: 4g") != std::string::npos) {
d.modePresetIdx = 3;
} else if (d.currentModes.find("allowed: 3g") != std::string::npos) {
d.modePresetIdx = 4;
} else if (d.currentModes.find("allowed: 2g") != std::string::npos) {
d.modePresetIdx = 5;
}
// Parse unlock-retries if present, e.g. "sim-pin (3), sim-puk (10)"
for (int idx = 1; idx <= 6; ++idx) {
std::string rk = "modem.generic.unlock-retries.value[" + std::to_string(idx) + "]";
std::string rv = cleanMmVal(mmKv[rk]);
if (rv.empty()) break;
if (rv.find("sim-pin (") != std::string::npos) {
size_t p = rv.find('(');
if (p != std::string::npos) d.mmPinRetries = std::atoi(rv.c_str() + p + 1);
} else if (rv.find("sim-puk (") != std::string::npos) {
size_t p = rv.find('(');
if (p != std::string::npos) d.mmPukRetries = std::atoi(rv.c_str() + p + 1);
}
}
}
// 3. Query active SIM object if available
if (!d.simDbusPath.empty()) {
std::string simOut = runCaptureCmd(
"mmcli -i " + shellQuote(d.simDbusPath) + " -K 2>/dev/null");
std::unordered_map<std::string, std::string> simKv;
parseKvLines(simOut, simKv, ':');
d.simIccid = cleanMmVal(simKv["sim.properties.iccid"]);
d.simOperatorName = cleanMmVal(simKv["sim.properties.operator-name"]);
if (d.operatorName.empty() && !d.simOperatorName.empty()) {
d.operatorName = d.simOperatorName;
}
}
// Sync ModemManager lock info into active slot if QMI didn't report retries
int actIdx = (d.primaryGwSlot >= 1 && d.primaryGwSlot <= 2)
? (d.primaryGwSlot - 1)
: (d.preferredSlot - 1);
if (actIdx >= 0 && actIdx < 2) {
if (d.mmUnlockRequired == "sim-pin" && d.slots[actIdx].pin1State == "not-initialized") {
d.slots[actIdx].state = "present";
d.slots[actIdx].pin1State = "enabled-not-verified";
} else if (d.mmUnlockRequired == "sim-puk") {
d.slots[actIdx].state = "present";
d.slots[actIdx].pin1State = "blocked";
}
if (d.mmPinRetries >= 0 && d.slots[actIdx].pin1Retries == 0) {
d.slots[actIdx].pin1Retries = d.mmPinRetries;
}
if (d.mmPukRetries >= 0 && d.slots[actIdx].puk1Retries == 0) {
d.slots[actIdx].puk1Retries = d.mmPukRetries;
}
}
return d;
}
std::vector<ModemLogRow> loadRecentModemLog(const ContactBook& book,
size_t maxEntries = 80) {
std::vector<ModemLogRow> out;
std::ifstream in(getModemLogFilePath());
if (!in.is_open()) return out;
std::vector<std::string> lines;
std::string line;
while (std::getline(in, line)) {
if (!line.empty()) lines.push_back(line);
}
for (auto it = lines.rbegin();
it != lines.rend() && out.size() < maxEntries; ++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);
ModemLogRow row;
row.timeStr = formatShortTime(trimStr(it->substr(0, t1)));
row.eventType = trimStr(it->substr(t1 + 1, t2 - t1 - 1));
std::string rawNum = trimStr(
t3 == std::string::npos ? it->substr(t2 + 1)
: it->substr(t2 + 1, t3 - t2 - 1));
std::string cname = book.resolveName(rawNum);
row.target = cname.empty() ? rawNum : (cname + " (" + rawNum + ")");
if (t3 != std::string::npos) {
row.detail = trimStr(it->substr(t3 + 1));
}
out.push_back(row);
}
return out;
}
int runModemApp(int argc, char** argv) {
gRunning = 1;
std::setlocale(LC_ALL, "");
XSetLocaleModifiers("");
bool isPreload = false;
std::string openArg;
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.empty()) {
openArg = argv[i];
}
}
std::string daemonPidPath = getPluginDaemonPidPath("modem");
std::string activePidPath = getRuntimeFile("xmodem.pid");
if (isPluginDaemonAlive("modem")) {
if (!isPreload) {
sendPluginCommand("modem",
openArg.empty() ? "OPEN" : ("OPEN " + openArg));
}
return 0;
}
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
int fifoFd = setupPluginDaemonFifo("modem");
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[] = "xmodem";
char resClass[] = "XModem";
ch.res_name = resName;
ch.res_class = resClass;
XSetClassHint(dpy, win, &ch);
XStoreName(dpy, win, "Modem & SIM");
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;
auto& colRed = pt.colRed;
ContactBook book;
ModemStatusData status;
std::vector<ModemLogRow> logRows;
TabId activeTab = TabId::SimCards;
ModalMode modal = ModalMode::None;
int simRowSel = 0; // 0 = SIM 1, 1 = SIM 2, 2 = Switch Active, 3 = Unlock PIN/PUK, 4 = PIN Lock Toggle, 5 = Change PIN
int radioRowSel = 0; // 0 = Network Mode, 1 = USSD, 2 = Modem Power, 3 = Restart MM, 4 = Refresh
int logRowSel = 0;
int actionSel = 0;
int targetSlot = 1;
// PIN / PUK / Change PIN wizard state
int wizardStep = 0;
bool revealDigits = false;
std::string inputPin1;
std::string inputPin2;
std::string inputPin3;
std::string modalError;
// USSD state
std::string ussdInput = "*100#";
std::string ussdResponse;
bool ussdWaitingReply = false;
// AT Console state
int atFocusRow = 0; // 0 = Input, 1 = Preset/Port, 2 = Output scroll
int atPresetIdx = 0;
int atPortIdx = 0; // 0 = auto, 1 = wwan0at0, 2 = wwan0at1
std::string atInput = "AT";
std::string atOutput = "Enter an AT command and press Send.\nUse Preset row (< >) for common commands.";
int atScrollLine = 0;
AtTextMode atTextMode = AtTextMode::Upper;
int atLastTapKey = -1;
int atLastTapIdx = 0;
int64_t atLastTapMs = 0;
std::string statusBanner;
int64_t statusBannerUntilMs = 0;
bool visible = false;
int64_t openStartMs = 0;
int64_t startupIgnoreUntilMs = 0;
int64_t lastRefreshMs = 0;
bool initialSoftkeysReleased = true;
int softkeyPending = 0;
int64_t softkeyDeadlineMs = 0;
auto setBanner = [&](const std::string& msg, int durationMs = 2400) {
statusBanner = msg;
statusBannerUntilMs = nowMs() + durationMs;
};
auto refreshData = [&](bool reloadLog = false) {
status = queryModemAndSimStatus();
if (reloadLog || activeTab == TabId::ModemLog) {
book.load();
logRows = loadRecentModemLog(book);
if (logRowSel >= static_cast<int>(logRows.size())) {
logRowSel = std::max(0, static_cast<int>(logRows.size()) - 1);
}
}
lastRefreshMs = nowMs();
};
auto openPinOrPukModalForSlot = [&](int slotNum) {
targetSlot = std::clamp(slotNum, 1, 2);
const auto& s = status.slots[targetSlot - 1];
wizardStep = 0;
revealDigits = false;
inputPin1.clear();
inputPin2.clear();
inputPin3.clear();
modalError.clear();
if (s.needsPuk() || status.mmUnlockRequired == "sim-puk") {
modal = ModalMode::UnblockPuk;
} else {
modal = ModalMode::VerifyPin;
}
};
auto applyOpenArg = [&](const std::string& arg) {
std::string a = toLowerStr(trimStr(arg));
modal = ModalMode::None;
if (a == "unlock" || a == "pin" || a == "puk") {
activeTab = TabId::SimCards;
int lockSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2)
? status.primaryGwSlot
: status.preferredSlot;
if (status.slots[0].needsPin() || status.slots[0].needsPuk()) {
lockSlot = 1;
} else if (status.slots[1].needsPin() || status.slots[1].needsPuk()) {
lockSlot = 2;
}
simRowSel = lockSlot - 1;
openPinOrPukModalForSlot(lockSlot);
} else if (a == "info" || a == "status" || a == "radio") {
activeTab = TabId::RadioInfo;
} else if (a == "log" || a == "logs") {
activeTab = TabId::ModemLog;
book.load();
logRows = loadRecentModemLog(book);
} else if (a == "at" || a == "atcmd") {
activeTab = TabId::AtConsole;
} else if (a == "ussd") {
activeTab = TabId::RadioInfo;
modal = ModalMode::UssdDialog;
ussdResponse.clear();
ussdWaitingReply = false;
} else {
activeTab = TabId::SimCards;
// If active SIM is locked, auto-highlight it
if (status.slots[0].needsPin() || status.slots[0].needsPuk()) {
simRowSel = 0;
} else if (status.slots[1].needsPin() || status.slots[1].needsPuk()) {
simRowSel = 1;
}
}
};
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 drawSignalBars = [&](int x, int y, int pct) {
int bars = (pct <= 0) ? 0 : (pct < 25 ? 1 : (pct < 50 ? 2 : (pct < 75 ? 3 : 4)));
for (int i = 0; i < 4; ++i) {
int bh = 4 + i * 3;
int bx = x + i * 5;
int by = y + (14 - bh);
XSetForeground(dpy, gc, (i < bars) ? pt.spec.success : pt.spec.border);
XFillRectangle(dpy, pix, gc, bx, by, 3, bh);
}
};
auto maskPinStr = [&](const std::string& s) -> std::string {
if (revealDigits) return s;
return std::string(s.size(), '*');
};
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[4] = {"1:SIMs", "2:Radio", "3:AT", "4:Log"};
int tabW = kWinWidth / 4;
for (int t = 0; t < 4; ++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 == TabId::SimCards) {
// Draw SIM 1 & SIM 2 Cards (height 68px each)
for (int i = 0; i < 2; ++i) {
const auto& s = status.slots[i];
int cardY = contentY + i * 70;
bool rowSelected = (simRowSel == i);
XSetForeground(dpy, gc, rowSelected ? pt.spec.selBg : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 8, 66);
XSetForeground(dpy, gc, rowSelected ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 9, 65);
drawPango(layoutIcon, s.state == "present" ? &colGreen : &colMuted,
10, cardY + 2, 22, kIconSim);
std::string title = "SIM Slot " + std::to_string(s.slotNum);
if (s.isActiveProvisioned) {
title += " [ACTIVE]";
} else if (s.isPreferred) {
title += " [PREF]";
}
drawPango(layoutMain, rowSelected ? &colWhite : &colLight,
30, cardY + 2, 22, title, 198);
std::string line2;
XftColor* line2Col = &colMuted;
if (s.state == "present") {
if (s.needsPuk()) {
line2 = "BLOCKED (Enter PUK)";
line2Col = &colRed;
} else if (s.needsPin()) {
line2 = "LOCKED (Enter PIN)";
line2Col = &colOrange;
} else if (s.isActiveProvisioned) {
std::string op = status.operatorName.empty() ? "Ready" : status.operatorName;
if (!status.ownNumber.empty()) op += " " + status.ownNumber;
line2 = op;
line2Col = &colGreen;
} else {
line2 = "Present (Standby " + s.appType + ")";
line2Col = &colBlue;
}
} else if (s.state == "error") {
line2 = "No SIM (No ATR)";
} else {
line2 = "Empty Slot";
}
drawPango(layoutSmall, line2Col, 10, cardY + 24, 20, line2, 220);
std::string line3;
if (s.state == "present") {
std::string pState = s.pin1State;
if (pState == "enabled-not-verified") pState = "PIN Req";
else if (pState == "enabled-verified") pState = "PIN On";
else if (pState == "disabled") pState = "PIN Off";
else if (pState == "blocked") pState = "PUK Req";
line3 = pState + " (PIN:" + std::to_string(s.pin1Retries) +
" PUK:" + std::to_string(s.puk1Retries) + ")";
} else {
line3 = "Insert nano-SIM & restart/switch";
}
drawPango(layoutSmall, rowSelected ? &colLight : &colMuted,
10, cardY + 44, 18, line3, 220);
}
// Quick Action Rows below the 2 SIM cards (4 rows, 24px each)
int actSlot = (simRowSel == 1) ? 2 : 1;
int otherSlot = (status.preferredSlot == 1) ? 2 : 1;
std::string quickRows[4] = {
std::string(kIconRefresh) + " Switch Active to SIM " + std::to_string(otherSlot),
std::string(kIconUnlock) + " Unlock SIM (PIN / PUK)",
std::string(kIconLock) + " Enable / Disable SIM PIN",
std::string(kIconKey) + " Change SIM PIN Code",
};
(void)actSlot;
int baseY = contentY + 142;
for (int r = 0; r < 4; ++r) {
int ry = baseY + r * 23;
bool isSel = (simRowSel == (r + 2));
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : ((r % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd));
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 22);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
10, ry, 22, quickRows[r], kWinWidth - 20);
}
drawSoftkeyBar("Actions", "Select", "Close");
} else if (activeTab == TabId::RadioInfo) {
// Modem summary card (height 102px)
XSetForeground(dpy, gc, pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 8, 102);
XSetForeground(dpy, gc, pt.spec.border);
XDrawRectangle(dpy, pix, gc, 4, contentY, kWinWidth - 9, 101);
std::string stStr = status.mmPresent ? status.mmState : "nomodem";
if (!status.mmFailedReason.empty()) {
stStr += " (" + status.mmFailedReason + ")";
}
XftColor* stCol = &colRed;
if (status.mmState == "connected" || status.mmState == "registered") {
stCol = &colGreen;
} else if (status.mmState == "searching" || status.mmState == "connecting") {
stCol = &colOrange;
}
drawPango(layoutMain, stCol, 10, contentY + 2, 22,
"State: " + stStr, 185);
drawSignalBars(kWinWidth - 32, contentY + 6, status.signalQuality);
std::string opLine = "Operator: " +
(status.operatorName.empty() ? "--" : status.operatorName);
if (!status.accessTech.empty()) {
opLine += " [" + status.accessTech + "]";
}
drawPango(layoutSmall, &colLight, 10, contentY + 24, 18, opLine, 220);
std::string sigLine = "Signal: " + std::to_string(status.signalQuality) +
"% | Slot: SIM " +
(status.primaryGwSlot > 0 ? std::to_string(status.primaryGwSlot) : "--");
drawPango(layoutSmall, &colLight, 10, contentY + 42, 18, sigLine, 220);
std::string imei1Line = "IMEI 1: " + (status.imei.empty() ? "--" : status.imei);
drawPango(layoutSmall, &colMuted, 10, contentY + 60, 18, imei1Line, 220);
std::string imei2Line = "IMEI 2: " + (status.imei2.empty() ? "--" : status.imei2);
drawPango(layoutSmall, &colMuted, 10, contentY + 78, 18, imei2Line, 220);
// 5 Interactive Radio Control Rows
std::string modeLabel = kRadioModes[status.modePresetIdx].label;
bool powerOn = (status.mmPowerState == "on" || status.mmState == "registered" ||
status.mmState == "connected" || status.mmState == "searching");
std::string radioRows[5] = {
std::string(kIconSignal) + " Mode: < " + modeLabel + " >",
std::string(kIconPhone) + " USSD Code (" + ussdInput + ")",
std::string(kIconFlash) + " Radio Power: " + (powerOn ? "ON" : "OFF"),
std::string(kIconRefresh) + " Restart ModemManager",
std::string(kIconInfo) + " Refresh Status Now",
};
int baseY = contentY + 106;
for (int r = 0; r < 5; ++r) {
int ry = baseY + r * 25;
bool isSel = (radioRowSel == 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, 24);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
10, ry, 24, radioRows[r], kWinWidth - 20);
}
drawSoftkeyBar("Refresh", "Apply", "Close");
} else if (activeTab == TabId::AtConsole) {
const char* modeBadge = "[ABC]";
if (atTextMode == AtTextMode::Lower) modeBadge = "[abc]";
else if (atTextMode == AtTextMode::Num) modeBadge = "[123]";
else if (atTextMode == AtTextMode::Sym) modeBadge = "[Sym]";
const char* portNames[3] = {"auto", "wwan0at0", "wwan0at1"};
const char* portLabels[3] = {"Auto", "SIM1 (at0)", "SIM2 (at1)"};
(void)portNames;
// Row 0: AT Command Input Box (Y=contentY..contentY+46)
drawPango(layoutSmall, &colYellow, 8, contentY, 18,
std::string(kIconTerm) + " AT Command (# mode, * +?=):");
int mbW = pangoTextWidth(layoutSmall, modeBadge);
drawPango(layoutSmall, &colGreen, kWinWidth - mbW - 8, contentY, 18, modeBadge);
bool focus0 = (atFocusRow == 0);
XSetForeground(dpy, gc, focus0 ? pt.spec.bgInputFocus : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 12, 26);
XSetForeground(dpy, gc, focus0 ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 6, contentY + 19, kWinWidth - 13, 25);
drawPango(layoutMain, &colWhite, 12, contentY + 19, 26,
atInput + (focus0 ? "_" : ""), kWinWidth - 24);
// Row 1: Preset & AT Port Selector (Y=contentY+49..contentY+73)
bool focus1 = (atFocusRow == 1);
XSetForeground(dpy, gc, focus1 ? pt.spec.selBg : pt.spec.bgAltOdd);
XFillRectangle(dpy, pix, gc, 6, contentY + 48, kWinWidth - 12, 24);
std::string presetRow = "< Preset: " + std::string(kAtPresets[atPresetIdx]) +
" | *Port: " + portLabels[atPortIdx] + " >";
drawPango(layoutSmall, focus1 ? &colWhite : &colLight,
10, contentY + 48, 24, presetRow, kWinWidth - 20);
// Row 2: AT Response Output Area (Y=contentY+75..contentY+contentH-2)
bool focus2 = (atFocusRow == 2);
int outY = contentY + 75;
int outH = contentH - 77;
XSetForeground(dpy, gc, pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 6, outY, kWinWidth - 12, outH);
XSetForeground(dpy, gc, focus2 ? pt.spec.accent : pt.spec.border);
XDrawRectangle(dpy, pix, gc, 6, outY, kWinWidth - 13, outH - 1);
pango_layout_set_width(layoutSmall, (kWinWidth - 24) * PANGO_SCALE);
pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR);
pango_layout_set_ellipsize(layoutSmall, PANGO_ELLIPSIZE_NONE);
pango_layout_set_text(layoutSmall, atOutput.c_str(),
static_cast<int>(atOutput.size()));
int lineCount = pango_layout_get_line_count(layoutSmall);
int maxVisLines = std::max(1, (outH - 8) / 17);
if (atScrollLine > std::max(0, lineCount - maxVisLines)) {
atScrollLine = std::max(0, lineCount - maxVisLines);
}
int curY = outY + 4;
for (int li = atScrollLine;
li < lineCount && (li - atScrollLine) < maxVisLines; ++li) {
PangoLayoutLine* pl = pango_layout_get_line_readonly(layoutSmall, li);
if (pl) {
PangoRectangle ink{}, logical{};
pango_layout_line_get_pixel_extents(pl, &ink, &logical);
pango_xft_render_layout_line(
xftDraw, &colLight, pl, 12 * PANGO_SCALE,
(curY - logical.y) * PANGO_SCALE);
curY += std::max(16, logical.height + 1);
}
}
pango_layout_set_width(layoutSmall, -1);
drawSoftkeyBar("Send", "Send",
(atFocusRow == 0 && !atInput.empty()) ? "Clear" : "Close");
} else if (activeTab == TabId::ModemLog) {
if (logRows.empty()) {
drawPango(layoutMain, &colMuted, 16, contentY + 60, 24,
"No modem events logged.");
} else {
const int rowH = 38;
const int maxVis = contentH / rowH; // 6 rows
int startIdx = std::clamp(logRowSel - maxVis / 2, 0,
std::max(0, static_cast<int>(logRows.size()) - maxVis));
for (int i = 0; i < maxVis && (startIdx + i) < static_cast<int>(logRows.size()); ++i) {
int idx = startIdx + i;
const auto& row = logRows[idx];
int ry = contentY + i * rowH;
bool isSel = (idx == logRowSel);
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
: ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd));
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1);
const char* ic = kIconInfo;
XftColor* icCol = &colBlue;
if (row.eventType.find("missed") != std::string::npos) {
ic = kIconMissedCall;
icCol = &colRed;
} else if (row.eventType.find("call_") != std::string::npos) {
ic = kIconPhone;
icCol = &colGreen;
} else if (row.eventType.find("txt") != std::string::npos) {
ic = kIconSms;
icCol = &colYellow;
}
drawPango(layoutIcon, icCol, 6, ry + 2, 18, ic);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
26, ry + 2, 18, row.eventType + " | " + row.timeStr, 208);
std::string sub = row.target;
if (!row.detail.empty()) sub += " - " + row.detail;
drawPango(layoutSmall, isSel ? &colLight : &colMuted,
26, ry + 19, 17, sub, 208);
}
}
drawSoftkeyBar("Refresh", "Call/SMS", "Close");
}
// Modal Overlays
if (modal == ModalMode::SimActions) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 12, 44, kWinWidth - 24, 196);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 12, 44, kWinWidth - 25, 195);
std::string hdr = "SIM Slot " + std::to_string(targetSlot) + " Actions";
drawPango(layoutMain, &colYellow, 20, 48, 24, hdr, kWinWidth - 40);
const char* actLabels[5] = {
"Set as Active SIM Slot",
"Unlock SIM (Enter PIN)",
"Unblock SIM (Enter PUK)",
"Enable / Disable SIM PIN",
"Change SIM PIN Code",
};
for (int i = 0; i < 5; ++i) {
int ry = 76 + i * 30;
bool isSel = (actionSel == i);
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgButton);
XFillRectangle(dpy, pix, gc, 18, ry, kWinWidth - 36, 26);
drawPango(layoutSmall, isSel ? &colWhite : &colLight,
24, ry, 26, actLabels[i], kWinWidth - 48);
}
drawSoftkeyBar("Select", "OK", "Back");
} else if (modal == ModalMode::VerifyPin ||
modal == ModalMode::UnblockPuk ||
modal == ModalMode::TogglePinLock ||
modal == ModalMode::ChangePin) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 10, 40, kWinWidth - 20, 210);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 10, 40, kWinWidth - 21, 209);
const auto& s = status.slots[targetSlot - 1];
std::string title;
std::string prompt;
std::string activeInput;
std::string retriesText;
if (modal == ModalMode::VerifyPin) {
title = "Unlock SIM " + std::to_string(targetSlot) + " (PIN)";
prompt = "Enter 4-8 digit SIM PIN:";
activeInput = maskPinStr(inputPin1);
retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries);
} else if (modal == ModalMode::UnblockPuk) {
title = "Unblock SIM " + std::to_string(targetSlot) + " (PUK)";
retriesText = "PUK attempts remaining: " + std::to_string(s.puk1Retries);
if (wizardStep == 0) {
prompt = "Step 1/3: Enter 8-digit PUK:";
activeInput = revealDigits ? inputPin1 : inputPin1; // PUK is usually visible or toggleable
} else if (wizardStep == 1) {
prompt = "Step 2/3: Enter New PIN (4-8 digits):";
activeInput = maskPinStr(inputPin2);
} else {
prompt = "Step 3/3: Confirm New PIN:";
activeInput = maskPinStr(inputPin3);
}
} else if (modal == ModalMode::TogglePinLock) {
bool enableTarget = !s.isPinEnabled();
title = (enableTarget ? "Enable" : "Disable") +
std::string(" SIM ") + std::to_string(targetSlot) + " PIN";
prompt = "Enter current SIM PIN:";
activeInput = maskPinStr(inputPin1);
retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries);
} else if (modal == ModalMode::ChangePin) {
title = "Change SIM " + std::to_string(targetSlot) + " PIN";
retriesText = "PIN attempts remaining: " + std::to_string(s.pin1Retries);
if (wizardStep == 0) {
prompt = "Step 1/3: Current SIM PIN:";
activeInput = maskPinStr(inputPin1);
} else if (wizardStep == 1) {
prompt = "Step 2/3: New SIM PIN (4-8 digits):";
activeInput = maskPinStr(inputPin2);
} else {
prompt = "Step 3/3: Confirm New SIM PIN:";
activeInput = maskPinStr(inputPin3);
}
}
drawPango(layoutMain, &colYellow, 18, 46, 24, title, kWinWidth - 36);
drawPango(layoutSmall, (s.pin1Retries <= 1 && s.pin1Retries > 0) ? &colRed : &colOrange,
18, 72, 20, retriesText, kWinWidth - 36);
drawPango(layoutSmall, &colLight, 18, 98, 20, prompt, kWinWidth - 36);
// Input box
XSetForeground(dpy, gc, pt.spec.bgInputFocus);
XFillRectangle(dpy, pix, gc, 18, 122, kWinWidth - 36, 34);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 18, 122, kWinWidth - 37, 33);
std::string dispBox = activeInput + "_";
drawPango(layoutMain, &colWhite, 26, 122, 34, dispBox, kWinWidth - 52);
drawPango(layoutSmall, &colMuted, 18, 164, 18,
"Press [*] to Show/Hide digits", kWinWidth - 36);
drawPango(layoutSmall, &colMuted, 18, 182, 18,
"Press [BackSpace] to delete digit", kWinWidth - 36);
if (!modalError.empty()) {
drawPango(layoutSmall, &colRed, 18, 204, 20, modalError, kWinWidth - 36);
}
drawSoftkeyBar("Submit", "OK", "Cancel");
} else if (modal == ModalMode::UssdDialog) {
XSetForeground(dpy, gc, pt.spec.bgOverlay);
XFillRectangle(dpy, pix, gc, 10, 40, kWinWidth - 20, 210);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 10, 40, kWinWidth - 21, 209);
drawPango(layoutMain, &colYellow, 18, 46, 24,
ussdWaitingReply ? "USSD Reply" : "Send USSD Code", kWinWidth - 36);
XSetForeground(dpy, gc, pt.spec.bgInputFocus);
XFillRectangle(dpy, pix, gc, 18, 74, kWinWidth - 36, 30);
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 18, 74, kWinWidth - 37, 29);
drawPango(layoutMain, &colWhite, 24, 74, 30, ussdInput + "_", kWinWidth - 48);
// Response area
XSetForeground(dpy, gc, pt.spec.bgInput);
XFillRectangle(dpy, pix, gc, 18, 110, kWinWidth - 36, 130);
std::string respText = ussdResponse.empty()
? "Dial e.g. *100# and press Send."
: ussdResponse;
pango_layout_set_width(layoutSmall, (kWinWidth - 48) * PANGO_SCALE);
pango_layout_set_wrap(layoutSmall, PANGO_WRAP_WORD_CHAR);
pango_layout_set_text(layoutSmall, respText.c_str(),
static_cast<int>(respText.size()));
pango_xft_render_layout(xftDraw, &colLight, layoutSmall,
24 * PANGO_SCALE, 116 * PANGO_SCALE);
drawSoftkeyBar("Send", "Send", "Close");
}
// Status Toast Banner
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("modem");
refreshData(true);
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;
int xfdLocal = ConnectionNumber(dpy);
int64_t drainDeadline = nowMs() + 180;
while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) {
while (XPending(dpy) > 0) {
XEvent ev;
XNextEvent(dpy, &ev);
}
struct pollfd pfd {};
pfd.fd = xfdLocal;
pfd.events = POLLIN;
::poll(&pfd, 1, 10);
}
writeSoftkeySuppress(nowMs() + 450);
XUngrabKeyboard(dpy, CurrentTime);
XUnmapWindow(dpy, win);
XFlush(dpy);
visible = false;
modal = ModalMode::None;
::unlink(activePidPath.c_str());
};
auto switchSimSlotNow = [&](int slotNum) {
slotNum = std::clamp(slotNum, 1, 2);
setBanner("Switching active SIM to Slot " + std::to_string(slotNum) + "...");
render();
int rc = 0;
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection switch " +
std::to_string(slotNum) + " >/dev/null 2>&1", &rc);
runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1");
refreshData(false);
if (rc == 0) {
setBanner("Active SIM set to Slot " + std::to_string(slotNum));
} else {
setBanner("Failed to switch SIM slot");
}
};
auto submitModalInput = [&]() {
if (modal == ModalMode::VerifyPin) {
if (inputPin1.size() < 4 || inputPin1.size() > 8) {
modalError = "PIN must be 4 to 8 digits";
return;
}
setBanner("Verifying SIM PIN...");
render();
int rc = -1;
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection verify-pin " +
std::to_string(targetSlot) + " " + shellQuote(inputPin1) +
" >/dev/null 2>&1", &rc);
refreshData(false);
runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1");
if (rc == 0) {
modal = ModalMode::None;
setBanner("SIM " + std::to_string(targetSlot) + " unlocked");
} else {
inputPin1.clear();
modalError = "Incorrect PIN";
}
} else if (modal == ModalMode::UnblockPuk) {
if (wizardStep == 0) {
if (inputPin1.size() != 8) {
modalError = "PUK must be 8 digits";
return;
}
modalError.clear();
wizardStep = 1;
} else if (wizardStep == 1) {
if (inputPin2.size() < 4 || inputPin2.size() > 8) {
modalError = "New PIN must be 4-8 digits";
return;
}
modalError.clear();
wizardStep = 2;
} else {
if (inputPin3 != inputPin2) {
inputPin3.clear();
modalError = "PIN confirmation does not match";
return;
}
setBanner("Unblocking SIM with PUK...");
render();
int rc = -1;
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection unblock-puk " +
std::to_string(targetSlot) + " " + shellQuote(inputPin1) +
" " + shellQuote(inputPin2) + " >/dev/null 2>&1", &rc);
refreshData(false);
runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1");
if (rc == 0) {
modal = ModalMode::None;
setBanner("SIM unblocked and new PIN set");
} else {
wizardStep = 0;
inputPin1.clear();
inputPin2.clear();
inputPin3.clear();
modalError = "PUK unblock failed";
}
}
} else if (modal == ModalMode::TogglePinLock) {
if (inputPin1.size() < 4 || inputPin1.size() > 8) {
modalError = "PIN must be 4 to 8 digits";
return;
}
bool enableTarget = !status.slots[targetSlot - 1].isPinEnabled();
std::string modeStr = enableTarget ? "enable" : "disable";
setBanner((enableTarget ? "Enabling" : "Disabling") + std::string(" SIM PIN..."));
render();
int rc = -1;
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection set-pin-lock " +
std::to_string(targetSlot) + " " + modeStr + " " +
shellQuote(inputPin1) + " >/dev/null 2>&1", &rc);
refreshData(false);
if (rc == 0) {
modal = ModalMode::None;
setBanner(std::string("SIM PIN protection ") +
(enableTarget ? "enabled" : "disabled"));
} else {
inputPin1.clear();
modalError = "Failed (wrong PIN?)";
}
} else if (modal == ModalMode::ChangePin) {
if (wizardStep == 0) {
if (inputPin1.size() < 4 || inputPin1.size() > 8) {
modalError = "Current PIN must be 4-8 digits";
return;
}
modalError.clear();
wizardStep = 1;
} else if (wizardStep == 1) {
if (inputPin2.size() < 4 || inputPin2.size() > 8) {
modalError = "New PIN must be 4-8 digits";
return;
}
modalError.clear();
wizardStep = 2;
} else {
if (inputPin3 != inputPin2) {
inputPin3.clear();
modalError = "New PINs do not match";
return;
}
setBanner("Changing SIM PIN...");
render();
int rc = -1;
runCaptureCmd("doas -n /usr/libexec/msm-modem-uim-selection change-pin " +
std::to_string(targetSlot) + " " + shellQuote(inputPin1) +
" " + shellQuote(inputPin2) + " >/dev/null 2>&1", &rc);
refreshData(false);
if (rc == 0) {
modal = ModalMode::None;
setBanner("SIM PIN changed successfully");
} else {
wizardStep = 0;
inputPin1.clear();
inputPin2.clear();
inputPin3.clear();
modalError = "Failed to change PIN";
}
}
} else if (modal == ModalMode::UssdDialog) {
if (ussdInput.empty()) return;
if (ussdInput == "*#06#") {
DeviceImeis im = queryDeviceImeis();
ussdResponse = "IMEI 1: " + (im.imei1.empty() ? "--" : im.imei1) +
"\nIMEI 2: " + (im.imei2.empty() ? "--" : im.imei2) +
"\nIMEI SV: " + (im.imeiSv.empty() ? "--" : im.imeiSv);
ussdWaitingReply = false;
return;
}
ussdResponse = "Sending USSD " + ussdInput + "...";
render();
ussdResponse = sendModemUssd(ussdInput, ussdWaitingReply, &ussdWaitingReply);
if (ussdWaitingReply) {
ussdInput.clear();
}
}
};
auto applyRadioModePreset = [&](int presetIdx) {
presetIdx = (presetIdx + kNumRadioModes) % kNumRadioModes;
status.modePresetIdx = presetIdx;
const auto& p = kRadioModes[presetIdx];
setBanner(std::string("Setting mode: ") + p.label);
render();
std::string cmd = "mmcli -m any --set-allowed-modes=" +
shellQuote(p.allowed);
if ( std::strcmp(p.preferred, "none") != 0) {
cmd += " --set-preferred-mode=" + shellQuote(p.preferred);
}
cmd += " >/dev/null 2>&1";
int rc = 0;
runCaptureCmd(cmd, &rc);
refreshData(false);
setBanner(rc == 0 ? (std::string("Mode: ") + p.label) : "Mode change failed");
};
auto triggerSimRowAction = [&](int row) {
if (row == 0 || row == 1) {
targetSlot = row + 1;
actionSel = 0;
modal = ModalMode::SimActions;
} else if (row == 2) {
int otherSlot = (status.preferredSlot == 1) ? 2 : 1;
switchSimSlotNow(otherSlot);
} else if (row == 3) {
int slot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2)
? status.primaryGwSlot
: status.preferredSlot;
openPinOrPukModalForSlot(slot);
} else if (row == 4) {
targetSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2)
? status.primaryGwSlot
: status.preferredSlot;
wizardStep = 0;
inputPin1.clear();
modalError.clear();
modal = ModalMode::TogglePinLock;
} else if (row == 5) {
targetSlot = (status.primaryGwSlot >= 1 && status.primaryGwSlot <= 2)
? status.primaryGwSlot
: status.preferredSlot;
wizardStep = 0;
inputPin1.clear();
inputPin2.clear();
inputPin3.clear();
modalError.clear();
modal = ModalMode::ChangePin;
}
};
auto executeAtCommand = [&]() {
std::string cmd = trimStr(atInput);
if (cmd.empty()) {
setBanner("AT command cannot be empty");
return;
}
const char* portArg = (atPortIdx == 1)
? "/dev/wwan0at0"
: (atPortIdx == 2 ? "/dev/wwan0at1" : "auto");
atOutput = "Sending: " + cmd + " ...";
atScrollLine = 0;
render();
std::string out = trimStr(runCaptureCmd(
"doas -n /usr/libexec/msm-modem-uim-selection at " +
shellQuote(portArg) + " " + shellQuote(cmd) + " 2>&1"));
if (out.empty()) {
atOutput = "> " + cmd + "\n(No response)";
} else {
atOutput = "> " + cmd + "\n" + out;
}
atScrollLine = 0;
setBanner("AT response received");
};
auto handleLeftSoftkey = [&]() {
if (modal == ModalMode::SimActions) {
int act = actionSel;
modal = ModalMode::None;
if (act == 0) {
switchSimSlotNow(targetSlot);
} else if (act == 1) {
wizardStep = 0;
inputPin1.clear();
modalError.clear();
modal = ModalMode::VerifyPin;
} else if (act == 2) {
wizardStep = 0;
inputPin1.clear();
inputPin2.clear();
inputPin3.clear();
modalError.clear();
modal = ModalMode::UnblockPuk;
} else if (act == 3) {
wizardStep = 0;
inputPin1.clear();
modalError.clear();
modal = ModalMode::TogglePinLock;
} else if (act == 4) {
wizardStep = 0;
inputPin1.clear();
inputPin2.clear();
inputPin3.clear();
modalError.clear();
modal = ModalMode::ChangePin;
}
return;
}
if (modal != ModalMode::None) {
submitModalInput();
return;
}
if (activeTab == TabId::SimCards) {
targetSlot = (simRowSel == 1) ? 2 : 1;
actionSel = 0;
modal = ModalMode::SimActions;
} else if (activeTab == TabId::RadioInfo) {
refreshData(false);
setBanner("Modem status refreshed");
} else if (activeTab == TabId::AtConsole) {
executeAtCommand();
} else if (activeTab == TabId::ModemLog) {
refreshData(true);
setBanner("Modem log refreshed");
}
};
auto handleRightSoftkey = [&]() {
if (modal != ModalMode::None) {
if (modal == ModalMode::UssdDialog) {
runAsyncCmd("mmcli -m any --3gpp-ussd-cancel >/dev/null 2>&1");
}
modal = ModalMode::None;
return;
}
hideWindow();
};
auto handleEnterKey = [&]() {
if (modal == ModalMode::SimActions) {
handleLeftSoftkey();
return;
}
if (modal != ModalMode::None) {
submitModalInput();
return;
}
if (activeTab == TabId::SimCards) {
triggerSimRowAction(simRowSel);
} else if (activeTab == TabId::RadioInfo) {
if (radioRowSel == 0) {
applyRadioModePreset(status.modePresetIdx + 1);
} else if (radioRowSel == 1) {
modal = ModalMode::UssdDialog;
ussdResponse.clear();
ussdWaitingReply = false;
} else if (radioRowSel == 2) {
bool powerOn = (status.mmPowerState == "on" ||
status.mmState == "registered" ||
status.mmState == "connected" ||
status.mmState == "searching");
setBanner(powerOn ? "Disabling modem..." : "Enabling modem...");
render();
runCaptureCmd(powerOn ? "mmcli -m any -d >/dev/null 2>&1"
: "mmcli -m any -e >/dev/null 2>&1");
refreshData(false);
runAsyncCmd("sxmo_hook_statusbar.sh modem >/dev/null 2>&1");
} else if (radioRowSel == 3) {
setBanner("Restarting ModemManager...");
render();
runCaptureCmd("doas -n rc-service modemmanager restart >/dev/null 2>&1");
refreshData(false);
setBanner("ModemManager restarted");
} else if (radioRowSel == 4) {
refreshData(false);
setBanner("Refreshed");
}
} else if (activeTab == TabId::AtConsole) {
if (atFocusRow == 1) {
atPortIdx = (atPortIdx + 1) % 3;
} else {
executeAtCommand();
}
} else if (activeTab == TabId::ModemLog) {
if (!logRows.empty() && logRowSel >= 0 &&
logRowSel < static_cast<int>(logRows.size())) {
const auto& r = logRows[logRowSel];
std::string num = normalizePhone(r.target);
if (!num.empty()) {
if (r.eventType.find("txt") != std::string::npos) {
hideWindow();
if (!sendPluginCommand("sms", "OPEN thread:" + num)) {
runAsyncArgv("xsms", "tailtextlog", num.c_str());
}
} else {
hideWindow();
if (!sendPluginCommand("dialer", "OPEN " + num)) {
runAsyncArgv("xdialer", num.c_str());
}
}
}
}
}
};
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;
}
// Modal numeric / code input handling
if (modal == ModalMode::VerifyPin ||
modal == ModalMode::UnblockPuk ||
modal == ModalMode::TogglePinLock ||
modal == ModalMode::ChangePin) {
char dch = mapDialKey(sym, kc, ev.xkey.state);
if (dch == '*') {
revealDigits = !revealDigits;
render();
continue;
}
std::string* curStr = &inputPin1;
size_t maxLen = (modal == ModalMode::UnblockPuk && wizardStep == 0) ? 8 : 8;
if ((modal == ModalMode::UnblockPuk || modal == ModalMode::ChangePin) &&
wizardStep == 1) {
curStr = &inputPin2;
} else if ((modal == ModalMode::UnblockPuk || modal == ModalMode::ChangePin) &&
wizardStep == 2) {
curStr = &inputPin3;
}
if (dch >= '0' && dch <= '9') {
if (curStr->size() < maxLen) {
curStr->push_back(dch);
modalError.clear();
render();
}
continue;
}
if (sym == XK_BackSpace || sym == XK_Delete) {
if (!curStr->empty()) {
curStr->pop_back();
modalError.clear();
render();
} else if (wizardStep > 0) {
--wizardStep;
render();
}
continue;
}
if (sym == XK_Return || sym == XK_KP_Enter) {
submitModalInput();
render();
continue;
}
continue;
}
if (modal == ModalMode::UssdDialog) {
char dch = mapDialKey(sym, kc, ev.xkey.state);
if (dch != '\0') {
if (ussdInput.size() < 32) {
ussdInput.push_back(dch);
render();
}
continue;
}
if (sym == XK_BackSpace || sym == XK_Delete) {
if (!ussdInput.empty()) {
ussdInput.pop_back();
render();
}
continue;
}
if (sym == XK_Return || sym == XK_KP_Enter ||
sym == XF86XK_Send || sym == XF86XK_Phone ||
sym == XF86XK_PickupPhone) {
submitModalInput();
render();
continue;
}
continue;
}
if (modal == ModalMode::SimActions) {
if (sym == XK_Up || sym == XK_KP_Up) {
actionSel = (actionSel + 4) % 5;
render();
} else if (sym == XK_Down || sym == XK_KP_Down) {
actionSel = (actionSel + 1) % 5;
render();
} else if (sym == XK_Return || sym == XK_KP_Enter) {
handleEnterKey();
render();
}
continue;
}
// AT Console text input when atFocusRow == 0
char dch = mapDialKey(sym, kc, ev.xkey.state);
if (activeTab == TabId::AtConsole && atFocusRow == 0) {
if (sym == XK_BackSpace || sym == XK_Delete) {
if (!atInput.empty()) {
atInput.pop_back();
atLastTapKey = -1;
render();
}
continue;
}
if (dch == '#') {
atTextMode = static_cast<AtTextMode>(
(static_cast<int>(atTextMode) + 1) % 4);
atLastTapKey = -1;
render();
continue;
}
if (dch == '*') {
const char* syms = "+?=,1\"':;/-_*#";
int len = static_cast<int>(std::strlen(syms));
if (atLastTapKey == 10 && (now - atLastTapMs) < 850 &&
!atInput.empty()) {
atLastTapIdx = (atLastTapIdx + 1) % len;
atInput.back() = syms[atLastTapIdx];
} else if (atInput.size() < 64) {
atLastTapKey = 10;
atLastTapIdx = 0;
atInput.push_back(syms[0]);
}
atLastTapMs = now;
render();
continue;
}
if (dch >= '0' && dch <= '9') {
int digit = dch - '0';
if (atTextMode == AtTextMode::Num) {
if (atInput.size() < 64) {
atInput.push_back(dch);
atLastTapKey = -1;
}
} else if (atTextMode == AtTextMode::Sym) {
const char* symTable = "+?=,.\"':;/-_";
char sc = symTable[digit % 10];
if (atInput.size() < 64) {
atInput.push_back(sc);
atLastTapKey = -1;
}
} else {
const char* seq = kAtTapMap[digit];
int len = static_cast<int>(std::strlen(seq));
if (atLastTapKey == digit && (now - atLastTapMs) < 850 &&
!atInput.empty()) {
atLastTapIdx = (atLastTapIdx + 1) % len;
char chOut = seq[atLastTapIdx];
if (atTextMode == AtTextMode::Lower) {
chOut = static_cast<char>(
std::tolower(static_cast<unsigned char>(chOut)));
}
atInput.back() = chOut;
} else if (atInput.size() < 64) {
atLastTapKey = digit;
atLastTapIdx = 0;
char chOut = seq[0];
if (atTextMode == AtTextMode::Lower) {
chOut = static_cast<char>(
std::tolower(static_cast<unsigned char>(chOut)));
}
atInput.push_back(chOut);
}
atLastTapMs = now;
}
render();
continue;
}
}
// Main View Navigation (4 tabs: 1:SIMs, 2:Radio, 3:AT, 4:Log)
if (dch == '1') {
activeTab = TabId::SimCards;
render();
continue;
} else if (dch == '2') {
activeTab = TabId::RadioInfo;
render();
continue;
} else if (dch == '3') {
activeTab = TabId::AtConsole;
render();
continue;
} else if (dch == '4') {
activeTab = TabId::ModemLog;
refreshData(true);
render();
continue;
} else if (activeTab == TabId::AtConsole && dch == '*') {
atPortIdx = (atPortIdx + 1) % 3;
render();
continue;
}
if (sym == XK_Left || sym == XK_KP_Left) {
if (activeTab == TabId::RadioInfo && radioRowSel == 0) {
applyRadioModePreset(status.modePresetIdx - 1);
} else if (activeTab == TabId::AtConsole && atFocusRow == 1) {
atPresetIdx = (atPresetIdx + kNumAtPresets - 1) % kNumAtPresets;
atInput = kAtPresets[atPresetIdx];
} else {
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 3) % 4);
if (activeTab == TabId::ModemLog) refreshData(true);
}
render();
} else if (sym == XK_Right || sym == XK_KP_Right) {
if (activeTab == TabId::RadioInfo && radioRowSel == 0) {
applyRadioModePreset(status.modePresetIdx + 1);
} else if (activeTab == TabId::AtConsole && atFocusRow == 1) {
atPresetIdx = (atPresetIdx + 1) % kNumAtPresets;
atInput = kAtPresets[atPresetIdx];
} else {
activeTab = static_cast<TabId>((static_cast<int>(activeTab) + 1) % 4);
if (activeTab == TabId::ModemLog) refreshData(true);
}
render();
} else if (sym == XK_Up || sym == XK_KP_Up) {
if (activeTab == TabId::SimCards) {
simRowSel = (simRowSel + 5) % 6;
} else if (activeTab == TabId::RadioInfo) {
radioRowSel = (radioRowSel + 4) % 5;
} else if (activeTab == TabId::AtConsole) {
if (atFocusRow == 2 && atScrollLine > 0) {
--atScrollLine;
} else {
atFocusRow = (atFocusRow + 2) % 3;
}
} else if (activeTab == TabId::ModemLog && !logRows.empty()) {
logRowSel = std::max(0, logRowSel - 1);
}
render();
} else if (sym == XK_Down || sym == XK_KP_Down) {
if (activeTab == TabId::SimCards) {
simRowSel = (simRowSel + 1) % 6;
} else if (activeTab == TabId::RadioInfo) {
radioRowSel = (radioRowSel + 1) % 5;
} else if (activeTab == TabId::AtConsole) {
if (atFocusRow == 2) {
++atScrollLine;
} else {
atFocusRow = (atFocusRow + 1) % 3;
}
} else if (activeTab == TabId::ModemLog && !logRows.empty()) {
logRowSel = std::min(static_cast<int>(logRows.size()) - 1, logRowSel + 1);
}
render();
} else if (sym == XK_Return || sym == XK_KP_Enter ||
sym == XF86XK_Send || sym == XF86XK_Phone ||
sym == XF86XK_PickupPhone) {
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);
}
}
// Periodic 5-second status refresh while visible on SIM/Radio tabs
if (modal == ModalMode::None && activeTab != TabId::ModemLog &&
activeTab != TabId::AtConsole) {
int64_t rRem = (lastRefreshMs + 5000) - now;
if (rRem <= 0) {
refreshData(false);
render();
rRem = 5000;
}
if (timeoutMs < 0 || rRem < timeoutMs) {
timeoutMs = static_cast<int>(rRem);
}
}
}
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("modem", "--preload");
}
return 0;
}
void pluginCleanup(void) {}
} // namespace
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
DWM_PLUGIN_API_VERSION,
"modem",
"Bananarchy Dual-SIM, PIN/PUK & Modem Manager (240x320)",
pluginInit,
pluginCleanup,
nullptr,
nullptr,
nullptr,
runModemApp
};