1504 lines
51 KiB
C++
1504 lines
51 KiB
C++
// 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();
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string shellQuote(const std::string& s) {
|
||
|
|
std::string out = "'";
|
||
|
|
for (char c : s) {
|
||
|
|
if (c == '\'') {
|
||
|
|
out += "'\\''";
|
||
|
|
} else {
|
||
|
|
out.push_back(c);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out.push_back('\'');
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
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.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;
|
||
|
|
auto& colRed = pt.colRed;
|
||
|
|
|
||
|
|
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", "Toggle/Edit", "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("nmcli connection delete USBNetworkShare >/dev/null 2>&1");
|
||
|
|
refreshCurrentTab(false);
|
||
|
|
setBanner("USB Tethering stopped");
|
||
|
|
} else {
|
||
|
|
setBanner("Starting USB Tethering...");
|
||
|
|
render();
|
||
|
|
runCaptureCmd("nmcli connection add type ethernet ifname usb0 "
|
||
|
|
"con-name USBNetworkShare ipv4.method shared >/dev/null 2>&1 && "
|
||
|
|
"nmcli connection up USBNetworkShare >/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
|
||
|
|
};
|