2026-10-24 19:58:11 +03:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-only
|
|
|
|
|
// Bananarchy Ringtone & Ringer Manager Plugin (ringtones.so / xringtones)
|
|
|
|
|
|
|
|
|
|
#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 <dirent.h>
|
|
|
|
|
#include <poll.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <sys/wait.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 {
|
|
|
|
|
Ringtones = 0,
|
|
|
|
|
Settings = 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct RingtoneEntry {
|
|
|
|
|
std::string path;
|
|
|
|
|
std::string displayName;
|
|
|
|
|
std::string formatTag;
|
|
|
|
|
std::string sourceTag;
|
|
|
|
|
off_t sizeBytes = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct RingSettings {
|
|
|
|
|
int ringerMode = 0; // 0 = Ring + Vibrate, 1 = Ring Only, 2 = Vibrate Only, 3 = Silent
|
|
|
|
|
int ringTimeSec = 20;
|
|
|
|
|
int ringLoops = 10;
|
|
|
|
|
int vibrateStrength = 1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
constexpr int kRingTimeSteps[] = {10, 15, 20, 25, 30, 45, 60};
|
|
|
|
|
constexpr int kNumRingTimeSteps =
|
|
|
|
|
static_cast<int>(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0]));
|
|
|
|
|
|
|
|
|
|
constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50};
|
|
|
|
|
constexpr int kNumRingLoopSteps =
|
|
|
|
|
static_cast<int>(sizeof(kRingLoopSteps) / sizeof(kRingLoopSteps[0]));
|
|
|
|
|
|
|
|
|
|
std::string getConfigDir() {
|
|
|
|
|
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
|
|
|
|
if (xdg && *xdg) {
|
|
|
|
|
return std::string(xdg) + "/sxmo";
|
|
|
|
|
}
|
|
|
|
|
return getHomeDirPath() + "/.config/sxmo";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isAudioExtension(const std::string& name, std::string* extUpperOut = nullptr) {
|
|
|
|
|
size_t dot = name.rfind('.');
|
|
|
|
|
if (dot == std::string::npos || dot + 1 >= name.size()) return false;
|
|
|
|
|
std::string ext = toLowerStr(name.substr(dot + 1));
|
|
|
|
|
if (ext == "wav" || ext == "ogg" || ext == "mp3" ||
|
|
|
|
|
ext == "flac" || ext == "opus" || ext == "m4a") {
|
|
|
|
|
if (extUpperOut) {
|
|
|
|
|
*extUpperOut = ext;
|
|
|
|
|
for (char& c : *extUpperOut) {
|
|
|
|
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string prettifyFilename(const std::string& filename) {
|
|
|
|
|
size_t dot = filename.rfind('.');
|
|
|
|
|
std::string stem = (dot == std::string::npos) ? filename : filename.substr(0, dot);
|
|
|
|
|
for (char& c : stem) {
|
|
|
|
|
if (c == '_' || c == '-') c = ' ';
|
|
|
|
|
}
|
|
|
|
|
return trimStr(stem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void scanDirectoryForRingtones(const std::string& dirPath,
|
|
|
|
|
const std::string& sourceTag,
|
|
|
|
|
std::vector<RingtoneEntry>& out) {
|
|
|
|
|
DIR* d = ::opendir(dirPath.c_str());
|
|
|
|
|
if (!d) return;
|
|
|
|
|
std::vector<std::string> names;
|
|
|
|
|
while (struct dirent* ent = ::readdir(d)) {
|
|
|
|
|
if (ent->d_name[0] == '.') continue;
|
|
|
|
|
names.emplace_back(ent->d_name);
|
|
|
|
|
}
|
|
|
|
|
::closedir(d);
|
|
|
|
|
std::sort(names.begin(), names.end());
|
|
|
|
|
|
|
|
|
|
for (const auto& name : names) {
|
|
|
|
|
std::string extTag;
|
|
|
|
|
if (!isAudioExtension(name, &extTag)) continue;
|
|
|
|
|
std::string fullPath = dirPath + "/" + name;
|
|
|
|
|
struct stat st {};
|
|
|
|
|
if (::stat(fullPath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) continue;
|
|
|
|
|
RingtoneEntry e;
|
|
|
|
|
e.path = fullPath;
|
|
|
|
|
e.displayName = prettifyFilename(name);
|
|
|
|
|
e.formatTag = extTag;
|
|
|
|
|
e.sourceTag = sourceTag;
|
|
|
|
|
e.sizeBytes = st.st_size;
|
|
|
|
|
out.push_back(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<RingtoneEntry> discoverRingtones() {
|
|
|
|
|
std::vector<RingtoneEntry> list;
|
|
|
|
|
scanDirectoryForRingtones("/usr/share/sxmo/ringtones", "System", list);
|
|
|
|
|
|
|
|
|
|
struct stat st {};
|
|
|
|
|
if (::stat("/usr/share/sxmo/ring.ogg", &st) == 0 && S_ISREG(st.st_mode)) {
|
|
|
|
|
RingtoneEntry defEntry;
|
|
|
|
|
defEntry.path = "/usr/share/sxmo/ring.ogg";
|
|
|
|
|
defEntry.displayName = "Sxmo Default Ring";
|
|
|
|
|
defEntry.formatTag = "OGG";
|
|
|
|
|
defEntry.sourceTag = "System";
|
|
|
|
|
defEntry.sizeBytes = st.st_size;
|
|
|
|
|
list.push_back(defEntry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string home = getHomeDirPath();
|
|
|
|
|
scanDirectoryForRingtones(home + "/Ringtones", "User", list);
|
|
|
|
|
scanDirectoryForRingtones(home + "/Music/Ringtones", "User", list);
|
|
|
|
|
scanDirectoryForRingtones(home + "/Music", "Music", list);
|
|
|
|
|
return list;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string loadActiveRingtonePath() {
|
|
|
|
|
std::string cfgPath = getConfigDir() + "/ringtone";
|
|
|
|
|
std::ifstream ifs(cfgPath);
|
|
|
|
|
if (ifs) {
|
|
|
|
|
std::string line;
|
|
|
|
|
if (std::getline(ifs, line)) {
|
|
|
|
|
line = trimStr(line);
|
|
|
|
|
struct stat st {};
|
|
|
|
|
if (!line.empty() && ::stat(line.c_str(), &st) == 0) {
|
|
|
|
|
return line;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const char* envRing = std::getenv("SXMO_RINGTONE");
|
|
|
|
|
if (envRing && *envRing) return envRing;
|
|
|
|
|
return "/usr/share/sxmo/ring.ogg";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool saveActiveRingtonePath(const std::string& path) {
|
|
|
|
|
std::string cfgDir = getConfigDir();
|
|
|
|
|
::mkdir(cfgDir.c_str(), 0755);
|
|
|
|
|
std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc);
|
|
|
|
|
if (!ofs) return false;
|
|
|
|
|
ofs << path << "\n";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RingSettings loadRingSettings() {
|
|
|
|
|
RingSettings s;
|
|
|
|
|
std::string cfgDir = getConfigDir();
|
|
|
|
|
bool noRing = (::access((cfgDir + "/.noring").c_str(), F_OK) == 0);
|
|
|
|
|
bool noVib = (::access((cfgDir + "/.novibrate").c_str(), F_OK) == 0);
|
|
|
|
|
if (!noRing && !noVib) s.ringerMode = 0;
|
|
|
|
|
else if (!noRing && noVib) s.ringerMode = 1;
|
|
|
|
|
else if (noRing && !noVib) s.ringerMode = 2;
|
|
|
|
|
else s.ringerMode = 3;
|
|
|
|
|
|
|
|
|
|
std::ifstream ifs(cfgDir + "/ringsettings");
|
|
|
|
|
std::string line;
|
|
|
|
|
while (std::getline(ifs, line)) {
|
|
|
|
|
line = trimStr(line);
|
|
|
|
|
if (line.empty() || line[0] == '#') continue;
|
|
|
|
|
auto eq = line.find('=');
|
|
|
|
|
if (eq == std::string::npos) continue;
|
|
|
|
|
std::string k = trimStr(line.substr(0, eq));
|
|
|
|
|
std::string v = trimStr(line.substr(eq + 1));
|
|
|
|
|
if (v.size() >= 2 && ((v.front() == '"' && v.back() == '"') ||
|
|
|
|
|
(v.front() == '\'' && v.back() == '\''))) {
|
|
|
|
|
v = v.substr(1, v.size() - 2);
|
|
|
|
|
}
|
|
|
|
|
int val = std::atoi(v.c_str());
|
|
|
|
|
if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) {
|
|
|
|
|
s.ringTimeSec = val;
|
|
|
|
|
} else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) {
|
|
|
|
|
s.ringLoops = val;
|
|
|
|
|
} else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) {
|
|
|
|
|
s.vibrateStrength = val;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void saveRingSettings(const RingSettings& s) {
|
|
|
|
|
std::string cfgDir = getConfigDir();
|
|
|
|
|
::mkdir(cfgDir.c_str(), 0755);
|
|
|
|
|
|
|
|
|
|
std::string noRingPath = cfgDir + "/.noring";
|
|
|
|
|
std::string noVibPath = cfgDir + "/.novibrate";
|
|
|
|
|
if (s.ringerMode == 0) {
|
|
|
|
|
::unlink(noRingPath.c_str());
|
|
|
|
|
::unlink(noVibPath.c_str());
|
|
|
|
|
} else if (s.ringerMode == 1) {
|
|
|
|
|
::unlink(noRingPath.c_str());
|
|
|
|
|
writeIntFile(noVibPath, 1);
|
|
|
|
|
} else if (s.ringerMode == 2) {
|
|
|
|
|
writeIntFile(noRingPath, 1);
|
|
|
|
|
::unlink(noVibPath.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
writeIntFile(noRingPath, 1);
|
|
|
|
|
writeIntFile(noVibPath, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::ofstream ofs(cfgDir + "/ringsettings", std::ios::trunc);
|
|
|
|
|
if (ofs) {
|
|
|
|
|
ofs << "SXMO_RINGTIME=" << s.ringTimeSec << "\n";
|
|
|
|
|
ofs << "SXMO_RINGNUMBER=" << s.ringLoops << "\n";
|
|
|
|
|
ofs << "SXMO_VIBRATE_STRENGTH=" << s.vibrateStrength << "\n";
|
|
|
|
|
}
|
|
|
|
|
runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string formatFileSize(off_t bytes) {
|
|
|
|
|
if (bytes < 1024) return std::to_string(bytes) + " B";
|
|
|
|
|
return std::to_string((bytes + 512) / 1024) + " KB";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int runRingtonesApp(int argc, char** argv) {
|
|
|
|
|
gRunning = 1;
|
|
|
|
|
std::setlocale(LC_ALL, "");
|
|
|
|
|
XSetLocaleModifiers("");
|
|
|
|
|
|
|
|
|
|
bool isPreload = false;
|
|
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
|
|
|
if (argv[i] && std::strcmp(argv[i], "--preload") == 0) {
|
|
|
|
|
isPreload = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string daemonPidPath = getPluginDaemonPidPath("ringtones");
|
|
|
|
|
std::string activePidPath = getRuntimeFile("xringtones.pid");
|
|
|
|
|
|
|
|
|
|
if (isPluginDaemonAlive("ringtones")) {
|
|
|
|
|
if (!isPreload) {
|
|
|
|
|
sendPluginCommand("ringtones", "OPEN");
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
|
|
|
|
int fifoFd = setupPluginDaemonFifo("ringtones");
|
|
|
|
|
|
|
|
|
|
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[] = "xringtones";
|
|
|
|
|
char resClass[] = "XRingtones";
|
|
|
|
|
ch.res_name = resName;
|
|
|
|
|
ch.res_class = resClass;
|
|
|
|
|
XSetClassHint(dpy, win, &ch);
|
|
|
|
|
XStoreName(dpy, win, "Ringtones");
|
|
|
|
|
|
|
|
|
|
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& colWhite = pt.colWhite;
|
|
|
|
|
auto& colLight = pt.colLight;
|
|
|
|
|
auto& colMuted = pt.colMuted;
|
|
|
|
|
auto& colYellow = pt.colYellow;
|
|
|
|
|
auto& colGreen = pt.colGreen;
|
|
|
|
|
auto& colBlue = pt.colBlue;
|
|
|
|
|
|
|
|
|
|
TabId activeTab = TabId::Ringtones;
|
|
|
|
|
std::vector<RingtoneEntry> tones;
|
|
|
|
|
std::string activePath;
|
|
|
|
|
RingSettings settings;
|
|
|
|
|
int selIdx = 0;
|
|
|
|
|
int settingsRow = 0;
|
|
|
|
|
pid_t previewPid = -1;
|
|
|
|
|
std::string previewPath;
|
|
|
|
|
|
|
|
|
|
std::string statusBanner;
|
|
|
|
|
int64_t statusBannerUntilMs = 0;
|
|
|
|
|
|
|
|
|
|
bool visible = false;
|
|
|
|
|
int64_t startupIgnoreUntilMs = 0;
|
|
|
|
|
bool initialSoftkeysReleased = true;
|
|
|
|
|
int softkeyPending = 0;
|
|
|
|
|
int64_t softkeyDeadlineMs = 0;
|
|
|
|
|
|
|
|
|
|
auto setBanner = [&](const std::string& msg, int durationMs = 2200) {
|
|
|
|
|
statusBanner = msg;
|
|
|
|
|
statusBannerUntilMs = nowMs() + durationMs;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto stopPreview = [&]() {
|
|
|
|
|
if (previewPid > 0) {
|
|
|
|
|
::kill(previewPid, SIGTERM);
|
|
|
|
|
::waitpid(previewPid, nullptr, WNOHANG);
|
|
|
|
|
previewPid = -1;
|
|
|
|
|
}
|
|
|
|
|
previewPath.clear();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto checkPreviewAlive = [&]() {
|
|
|
|
|
if (previewPid > 0) {
|
|
|
|
|
int st = 0;
|
|
|
|
|
pid_t r = ::waitpid(previewPid, &st, WNOHANG);
|
|
|
|
|
if (r == previewPid || (r < 0 && errno == ECHILD)) {
|
|
|
|
|
previewPid = -1;
|
|
|
|
|
previewPath.clear();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto togglePreview = [&](const std::string& path) {
|
|
|
|
|
checkPreviewAlive();
|
|
|
|
|
if (previewPid > 0 && previewPath == path) {
|
|
|
|
|
stopPreview();
|
|
|
|
|
setBanner("Preview stopped");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
stopPreview();
|
|
|
|
|
pid_t pid = ::fork();
|
|
|
|
|
if (pid == 0) {
|
|
|
|
|
int devNull = ::open("/dev/null", O_RDWR);
|
|
|
|
|
if (devNull >= 0) {
|
|
|
|
|
::dup2(devNull, STDIN_FILENO);
|
|
|
|
|
::dup2(devNull, STDOUT_FILENO);
|
|
|
|
|
::dup2(devNull, STDERR_FILENO);
|
|
|
|
|
if (devNull > 2) ::close(devNull);
|
|
|
|
|
}
|
|
|
|
|
::execlp("mpv", "mpv", "--no-resume-playback", "--quiet",
|
|
|
|
|
"--no-video", "--loop=2", path.c_str(), nullptr);
|
|
|
|
|
::_exit(127);
|
|
|
|
|
} else if (pid > 0) {
|
|
|
|
|
previewPid = pid;
|
|
|
|
|
previewPath = path;
|
|
|
|
|
setBanner("Playing preview...");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto refreshData = [&]() {
|
|
|
|
|
tones = discoverRingtones();
|
|
|
|
|
activePath = loadActiveRingtonePath();
|
|
|
|
|
settings = loadRingSettings();
|
|
|
|
|
if (selIdx >= static_cast<int>(tones.size())) {
|
|
|
|
|
selIdx = std::max(0, static_cast<int>(tones.size()) - 1);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y,
|
|
|
|
|
int h, const std::string& text, int maxW = -1,
|
|
|
|
|
int align = 0) {
|
|
|
|
|
if (text.empty()) return;
|
|
|
|
|
pango_layout_set_text(layout, text.c_str(), -1);
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
int tw = 0, th = 0;
|
|
|
|
|
pango_layout_get_pixel_size(layout, &tw, &th);
|
|
|
|
|
int tx = x;
|
|
|
|
|
if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2;
|
|
|
|
|
else if (align == 2 && maxW > 0) tx = x + maxW - tw;
|
|
|
|
|
int ty = y + (h - th) / 2;
|
|
|
|
|
pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE,
|
|
|
|
|
ty * PANGO_SCALE);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto drawSoftkeyBar = [&](const std::string& leftLabel,
|
|
|
|
|
const std::string& midLabel,
|
|
|
|
|
const std::string& rightLabel) {
|
|
|
|
|
int barY = kWinHeight - 24;
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24);
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.border);
|
|
|
|
|
XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY);
|
|
|
|
|
|
|
|
|
|
drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0);
|
|
|
|
|
drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1);
|
|
|
|
|
drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto render = [&]() {
|
|
|
|
|
if (!visible) return;
|
|
|
|
|
checkPreviewAlive();
|
|
|
|
|
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.bgMain);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
|
|
|
|
|
|
|
|
|
// Top tab header (0..26)
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26);
|
|
|
|
|
|
|
|
|
|
const char* tabNames[2] = {"1:Ringtones", "2:Settings"};
|
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
|
|
|
int tx = i * 120;
|
|
|
|
|
bool isAct = (static_cast<int>(activeTab) == i);
|
|
|
|
|
if (isAct) {
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.selBg);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, tx, 0, 120, 26);
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.accent);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, tx, 24, 120, 2);
|
|
|
|
|
}
|
|
|
|
|
drawPango(layoutSmall, isAct ? &colWhite : &colMuted, tx + 4, 0, 24,
|
|
|
|
|
tabNames[i], 112, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activeTab == TabId::Ringtones) {
|
|
|
|
|
// Active ringtone summary banner (y=28..50)
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.bgAltEven);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 22);
|
|
|
|
|
std::string actName = prettifyFilename(
|
|
|
|
|
activePath.substr(activePath.rfind('/') + 1));
|
|
|
|
|
drawPango(layoutSmall, &colGreen, 8, 28, 22,
|
|
|
|
|
std::string(kIconBell) + " Active: " + actName,
|
|
|
|
|
kWinWidth - 16);
|
|
|
|
|
|
|
|
|
|
// List area (y=52..246, 5 rows of 38px)
|
|
|
|
|
constexpr int kVisibleRows = 5;
|
|
|
|
|
int total = static_cast<int>(tones.size());
|
|
|
|
|
int startIdx = 0;
|
|
|
|
|
if (total > kVisibleRows) {
|
|
|
|
|
startIdx = std::clamp(selIdx - kVisibleRows / 2, 0,
|
|
|
|
|
total - kVisibleRows);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < kVisibleRows; ++i) {
|
|
|
|
|
int idx = startIdx + i;
|
|
|
|
|
if (idx >= total) break;
|
|
|
|
|
const auto& e = tones[idx];
|
|
|
|
|
int ry = 54 + i * 38;
|
|
|
|
|
bool isSel = (idx == selIdx);
|
|
|
|
|
bool isActive = (e.path == activePath);
|
|
|
|
|
bool isPlaying = (previewPid > 0 && previewPath == e.path);
|
|
|
|
|
|
|
|
|
|
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
|
|
|
|
|
: (i % 2 == 0 ? pt.spec.bgAltEven
|
|
|
|
|
: pt.spec.bgAltOdd));
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 36);
|
|
|
|
|
if (isActive) {
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.accent);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 4, ry, 3, 36);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 1, 19,
|
|
|
|
|
e.displayName, 152);
|
|
|
|
|
|
|
|
|
|
std::string badge;
|
|
|
|
|
XftColor* badgeCol = &colMuted;
|
|
|
|
|
if (isPlaying) {
|
|
|
|
|
badge = "[PLAYING]";
|
|
|
|
|
badgeCol = &colYellow;
|
|
|
|
|
} else if (isActive) {
|
|
|
|
|
badge = "[ACTIVE]";
|
|
|
|
|
badgeCol = &colGreen;
|
|
|
|
|
}
|
|
|
|
|
if (!badge.empty()) {
|
|
|
|
|
drawPango(layoutSmall, badgeCol, 162, ry + 2, 18, badge, 70, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string sub = e.formatTag + " | " + formatFileSize(e.sizeBytes) +
|
|
|
|
|
" | " + e.sourceTag + " ([*] Preview)";
|
|
|
|
|
drawPango(layoutSmall, isSel ? &colLight : &colMuted, 10, ry + 19, 16,
|
|
|
|
|
sub, kWinWidth - 20);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool curPlaying = (!tones.empty() && selIdx >= 0 &&
|
|
|
|
|
selIdx < static_cast<int>(tones.size()) &&
|
|
|
|
|
previewPid > 0 &&
|
|
|
|
|
previewPath == tones[selIdx].path);
|
|
|
|
|
drawSoftkeyBar(curPlaying ? "Stop" : "Play [*]", "Set Ring", "Close");
|
|
|
|
|
} else {
|
|
|
|
|
// Tab 2: Settings
|
|
|
|
|
const char* modeLabels[4] = {
|
|
|
|
|
"Ring + Vibrate",
|
|
|
|
|
"Ring Only",
|
|
|
|
|
"Vibrate Only",
|
|
|
|
|
"Silent",
|
|
|
|
|
};
|
|
|
|
|
std::string rows[5][2] = {
|
|
|
|
|
{"Ringer Mode",
|
|
|
|
|
"< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"},
|
|
|
|
|
{"Ring Duration",
|
|
|
|
|
"< " + std::to_string(settings.ringTimeSec) + " sec >"},
|
|
|
|
|
{"Ring Loop Count",
|
|
|
|
|
"< " + std::to_string(settings.ringLoops) + " loops >"},
|
|
|
|
|
{"Vibrate Strength",
|
|
|
|
|
"< Level " + std::to_string(settings.vibrateStrength) + " >"},
|
|
|
|
|
{"Test Vibration", "Press OK to vibrate"},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
|
|
|
int ry = 32 + i * 42;
|
|
|
|
|
bool isSel = (settingsRow == i);
|
|
|
|
|
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 38);
|
|
|
|
|
if (isSel) {
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.accent);
|
|
|
|
|
XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 37);
|
|
|
|
|
}
|
|
|
|
|
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 2, 17,
|
|
|
|
|
rows[i][0], kWinWidth - 20);
|
|
|
|
|
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 18, 19,
|
|
|
|
|
rows[i][1], kWinWidth - 20);
|
|
|
|
|
}
|
|
|
|
|
drawSoftkeyBar("Change", "Select", "Close");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!statusBanner.empty()) {
|
|
|
|
|
int by = kWinHeight - 46;
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
|
|
|
|
XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20);
|
|
|
|
|
XSetForeground(dpy, gc, pt.spec.accent);
|
|
|
|
|
XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19);
|
|
|
|
|
drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner,
|
|
|
|
|
kWinWidth - 20, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
|
|
|
|
XFlush(dpy);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto showWindow = [&]() {
|
|
|
|
|
closeOtherPlugins("ringtones");
|
|
|
|
|
refreshData();
|
|
|
|
|
for (size_t i = 0; i < tones.size(); ++i) {
|
|
|
|
|
if (tones[i].path == activePath) {
|
|
|
|
|
selIdx = static_cast<int>(i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
2026-10-24 21:11:57 +03:00
|
|
|
if (!visible) {
|
|
|
|
|
visible = true;
|
|
|
|
|
XMapRaised(dpy, win);
|
|
|
|
|
XSync(dpy, False);
|
|
|
|
|
} else {
|
|
|
|
|
XRaiseWindow(dpy, win);
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < 20; ++i) {
|
|
|
|
|
if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync,
|
|
|
|
|
CurrentTime) == GrabSuccess) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
usleep(2000);
|
|
|
|
|
}
|
|
|
|
|
XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime);
|
2026-10-24 19:58:11 +03:00
|
|
|
startupIgnoreUntilMs = nowMs() + 180;
|
|
|
|
|
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
|
|
|
|
softkeyPending = 0;
|
|
|
|
|
render();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto hideWindow = [&]() {
|
|
|
|
|
stopPreview();
|
|
|
|
|
if (!visible) return;
|
2026-10-24 21:11:57 +03:00
|
|
|
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);
|
2026-10-24 19:58:11 +03:00
|
|
|
visible = false;
|
|
|
|
|
softkeyPending = 0;
|
|
|
|
|
XUnmapWindow(dpy, win);
|
|
|
|
|
::unlink(activePidPath.c_str());
|
|
|
|
|
XFlush(dpy);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto adjustSettingRow = [&](int delta) {
|
|
|
|
|
if (settingsRow == 0) {
|
|
|
|
|
settings.ringerMode = (settings.ringerMode + 4 + delta) % 4;
|
|
|
|
|
saveRingSettings(settings);
|
|
|
|
|
setBanner("Ringer mode updated");
|
|
|
|
|
} else if (settingsRow == 1) {
|
|
|
|
|
int curIdx = 2;
|
|
|
|
|
for (int i = 0; i < kNumRingTimeSteps; ++i) {
|
|
|
|
|
if (kRingTimeSteps[i] == settings.ringTimeSec) {
|
|
|
|
|
curIdx = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
curIdx = (curIdx + kNumRingTimeSteps + delta) % kNumRingTimeSteps;
|
|
|
|
|
settings.ringTimeSec = kRingTimeSteps[curIdx];
|
|
|
|
|
saveRingSettings(settings);
|
|
|
|
|
} else if (settingsRow == 2) {
|
|
|
|
|
int curIdx = 2;
|
|
|
|
|
for (int i = 0; i < kNumRingLoopSteps; ++i) {
|
|
|
|
|
if (kRingLoopSteps[i] == settings.ringLoops) {
|
|
|
|
|
curIdx = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
curIdx = (curIdx + kNumRingLoopSteps + delta) % kNumRingLoopSteps;
|
|
|
|
|
settings.ringLoops = kRingLoopSteps[curIdx];
|
|
|
|
|
saveRingSettings(settings);
|
|
|
|
|
} else if (settingsRow == 3) {
|
|
|
|
|
settings.vibrateStrength =
|
|
|
|
|
((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1;
|
|
|
|
|
saveRingSettings(settings);
|
|
|
|
|
} else if (settingsRow == 4) {
|
2026-10-24 21:11:57 +03:00
|
|
|
int mag = (settings.vibrateStrength == 1)
|
|
|
|
|
? 20000
|
|
|
|
|
: (settings.vibrateStrength == 3 ? 65535 : 40000);
|
|
|
|
|
runAsyncCmd(
|
|
|
|
|
"for d in /dev/input/by-path/platform-vibrator-event "
|
|
|
|
|
"/dev/input/by-path/*vibrator*-event; do "
|
|
|
|
|
"[ -e \"$d\" ] && export SXMO_VIBRATE_DEV=\"$d\" && break; done; "
|
|
|
|
|
"sxmo_vibrate 600 " +
|
|
|
|
|
std::to_string(mag) + " >/dev/null 2>&1");
|
2026-10-24 19:58:11 +03:00
|
|
|
setBanner("Testing vibration...");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!isPreload) {
|
|
|
|
|
showWindow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char dch = mapDialKey(sym, kc, ev.xkey.state);
|
|
|
|
|
if (dch == '1') {
|
|
|
|
|
activeTab = TabId::Ringtones;
|
|
|
|
|
render();
|
|
|
|
|
continue;
|
|
|
|
|
} else if (dch == '2') {
|
|
|
|
|
activeTab = TabId::Settings;
|
|
|
|
|
render();
|
|
|
|
|
continue;
|
|
|
|
|
} else if (dch == '*' && activeTab == TabId::Ringtones &&
|
|
|
|
|
!tones.empty()) {
|
|
|
|
|
togglePreview(tones[selIdx].path);
|
|
|
|
|
render();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sym == XK_Left || sym == XK_KP_Left) {
|
|
|
|
|
if (activeTab == TabId::Settings && settingsRow < 4) {
|
|
|
|
|
adjustSettingRow(-1);
|
|
|
|
|
} else {
|
|
|
|
|
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
|
|
|
|
|
: TabId::Ringtones;
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
|
|
|
|
if (activeTab == TabId::Settings && settingsRow < 4) {
|
|
|
|
|
adjustSettingRow(1);
|
|
|
|
|
} else {
|
|
|
|
|
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
|
|
|
|
|
: TabId::Ringtones;
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
} else if (sym == XK_Up || sym == XK_KP_Up) {
|
|
|
|
|
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
|
|
|
|
selIdx = (selIdx + static_cast<int>(tones.size()) - 1) %
|
|
|
|
|
static_cast<int>(tones.size());
|
|
|
|
|
} else if (activeTab == TabId::Settings) {
|
|
|
|
|
settingsRow = (settingsRow + 4) % 5;
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
|
|
|
|
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
|
|
|
|
selIdx = (selIdx + 1) % static_cast<int>(tones.size());
|
|
|
|
|
} else if (activeTab == TabId::Settings) {
|
|
|
|
|
settingsRow = (settingsRow + 1) % 5;
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
|
|
|
|
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
|
|
|
|
activePath = tones[selIdx].path;
|
|
|
|
|
saveActiveRingtonePath(activePath);
|
|
|
|
|
setBanner("Ringtone set: " + tones[selIdx].displayName);
|
|
|
|
|
} else if (activeTab == TabId::Settings) {
|
|
|
|
|
adjustSettingRow(1);
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int timeoutMs = -1;
|
|
|
|
|
int64_t now = nowMs();
|
|
|
|
|
if (visible) {
|
|
|
|
|
if (checkPreviewAlive()) {
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
|
|
|
|
togglePreview(tones[selIdx].path);
|
|
|
|
|
} else if (activeTab == TabId::Settings) {
|
|
|
|
|
adjustSettingRow(1);
|
|
|
|
|
}
|
|
|
|
|
render();
|
|
|
|
|
} else if (pend == 2) {
|
|
|
|
|
hideWindow();
|
|
|
|
|
}
|
|
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (previewPid > 0 && (timeoutMs < 0 || timeoutMs > 400)) {
|
|
|
|
|
timeoutMs = 400;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
showWindow();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hideWindow();
|
|
|
|
|
if (fifoFd >= 0) ::close(fifoFd);
|
|
|
|
|
::unlink(daemonPidPath.c_str());
|
|
|
|
|
if (xftDraw) XftDrawDestroy(xftDraw);
|
|
|
|
|
XFreeGC(dpy, gc);
|
|
|
|
|
XFreePixmap(dpy, pix);
|
|
|
|
|
XDestroyWindow(dpy, win);
|
|
|
|
|
XCloseDisplay(dpy);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int pluginInit(const DwmHostApi* host) {
|
|
|
|
|
if (host && host->spawn_plugin) {
|
|
|
|
|
host->spawn_plugin("ringtones", "--preload");
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void pluginCleanup(void) {}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
|
|
|
|
DWM_PLUGIN_API_VERSION,
|
|
|
|
|
"ringtones",
|
|
|
|
|
"Bananarchy Ringtones & Ringer Manager (240x320)",
|
|
|
|
|
pluginInit,
|
|
|
|
|
pluginCleanup,
|
|
|
|
|
nullptr,
|
|
|
|
|
nullptr,
|
|
|
|
|
nullptr,
|
|
|
|
|
runRingtonesApp
|
|
|
|
|
};
|