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

946 lines
31 KiB
C++

// 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 ringVol = 100;
int ringTimeSec = 20;
int ringLoops = 10;
int vibrateStrength = 2;
};
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" ||
ext == "aac" || ext == "wma" || ext == "aiff" || ext == "aif" ||
ext == "mod" || ext == "xm" || ext == "it" || ext == "s3m" ||
ext == "mptm" || ext == "stm" || ext == "med" || ext == "okt" ||
ext == "669" || ext == "ult" || ext == "far" || ext == "mtm" ||
ext == "ptm" || ext == "amf" || ext == "dsm" || ext == "imf" ||
ext == "gdm" || ext == "stx" || ext == "umx" || ext == "mo3") {
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);
scanDirectoryForRingtones("/usr/share/sxmo/music", "Tracker", 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;
struct stat stDef {};
if (::stat("/usr/share/sxmo/ringtones/Banana_Chime.wav", &stDef) == 0) {
return "/usr/share/sxmo/ringtones/Banana_Chime.wav";
}
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;
} else if (k == "SXMO_RINGVOL" && val >= 0 && val <= 100) {
s.ringVol = (val / 5) * 5;
}
}
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";
ofs << "SXMO_RINGVOL=" << std::clamp(s.ringVol, 0, 100) << "\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();
settings = loadRingSettings();
int rvol = std::clamp(settings.ringVol, 0, 100);
int rx3 = (rvol <= 0) ? 0 : (60 + rvol * 34 / 100);
int mpvVol = rvol;
std::string volArg = "--volume=" + std::to_string(mpvVol);
runAsyncCmd("amixer -c 0 cset \"name='EAR_S'\" 0 >/dev/null 2>&1; "
"amixer -c 0 cset \"name='RX1 MIX1 INP1'\" 0 >/dev/null 2>&1; "
"amixer -c 0 cset \"name='PRI_MI2S_RX Audio Mixer MultiMedia1'\" 1 >/dev/null 2>&1; "
"amixer -c 0 cset \"name='SPK DAC Switch'\" 1 >/dev/null 2>&1; "
"amixer -c 0 cset \"name='RX3 MIX1 INP1'\" 3 >/dev/null 2>&1; "
"amixer -c 0 cset \"name='RX3 Digital Volume'\" " +
std::to_string(rx3) + " >/dev/null 2>&1; "
"wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1; "
"wpctl set-volume @DEFAULT_AUDIO_SINK@ " +
std::to_string(rvol) + "% >/dev/null 2>&1; "
"(sleep 0.25; amixer -c 0 cset \"name='RX3 Digital Volume'\" " +
std::to_string(rx3) + " >/dev/null 2>&1; "
"wpctl set-volume @DEFAULT_AUDIO_SINK@ " +
std::to_string(rvol) + "% >/dev/null 2>&1) &");
pid_t pid = ::fork();
if (pid == 0) {
std::string runtimeDir = "/run/user/" + std::to_string(::getuid());
const char* xdg = std::getenv("XDG_RUNTIME_DIR");
struct stat stRuntime {};
if (!xdg || !*xdg || ::stat(xdg, &stRuntime) != 0) {
::setenv("XDG_RUNTIME_DIR", runtimeDir.c_str(), 1);
}
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-config", "--no-terminal",
"--no-resume-playback", "--quiet", "--vo=null",
"--no-video", "--ao=pipewire,pulse,alsa",
"--audio-channels=stereo", "--audio-samplerate=48000",
"--volume-max=150", volArg.c_str(),
"--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",
};
const char* vibLabels[3] = {"1 (Light)", "2 (Medium)", "3 (Strong)"};
int vIdx = std::clamp(settings.vibrateStrength, 1, 3) - 1;
std::string rows[6][2] = {
{"Ringer Mode",
"< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"},
{"Ring Volume",
"< " + std::to_string(settings.ringVol) + "% >"},
{"Ring Duration",
"< " + std::to_string(settings.ringTimeSec) + " sec >"},
{"Ring Loop Count",
"< " + std::to_string(settings.ringLoops) + " loops >"},
{"Vibrate Strength",
"< Level " + std::string(vibLabels[vIdx]) + " >"},
{"Test Vibration", "Press OK to vibrate"},
};
for (int i = 0; i < 6; ++i) {
int ry = 30 + i * 35;
bool isSel = (settingsRow == i);
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven);
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 33);
if (isSel) {
XSetForeground(dpy, gc, pt.spec.accent);
XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 32);
}
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 1, 15,
rows[i][0], kWinWidth - 20);
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 15, 17,
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 = [&]() {
writeIntFile(activePidPath, static_cast<int>(::getpid()));
closeOtherPlugins("ringtones");
refreshData();
for (size_t i = 0; i < tones.size(); ++i) {
if (tones[i].path == activePath) {
selIdx = static_cast<int>(i);
break;
}
}
if (!visible) {
visible = true;
XMapRaised(dpy, win);
XSync(dpy, False);
} else {
XRaiseWindow(dpy, win);
}
grabPluginKeyboard(dpy, win);
startupIgnoreUntilMs = nowMs() + 180;
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
softkeyPending = 0;
render();
};
auto hideWindow = [&]() {
stopPreview();
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);
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) {
settings.ringVol = std::clamp(settings.ringVol + delta * 5, 0, 100);
saveRingSettings(settings);
if (previewPid > 0) {
int rx3 = (settings.ringVol <= 0) ? 0 : (60 + settings.ringVol * 34 / 100);
runAsyncCmd("amixer -c 0 cset \"name='RX3 Digital Volume'\" " +
std::to_string(rx3) + " >/dev/null 2>&1; "
"wpctl set-volume @DEFAULT_AUDIO_SINK@ " +
std::to_string(settings.ringVol) + "% >/dev/null 2>&1");
}
setBanner("Ring volume: " + std::to_string(settings.ringVol) + "%");
} else if (settingsRow == 2) {
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 == 3) {
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 == 4) {
settings.vibrateStrength =
((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1;
saveRingSettings(settings);
runAsyncCmd("SXMO_VIBRATE_FORCE=1 sxmo_vibrate 450 " +
std::to_string(settings.vibrateStrength) + " >/dev/null 2>&1");
setBanner("Vibration level: " + std::to_string(settings.vibrateStrength));
} else if (settingsRow == 5) {
runAsyncCmd("SXMO_VIBRATE_FORCE=1 sxmo_vibrate 800 " +
std::to_string(settings.vibrateStrength) + " >/dev/null 2>&1");
setBanner("Testing vibration (Level " +
std::to_string(settings.vibrateStrength) + ")...");
}
};
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) {
if (isAnotherPluginActive("ringtones")) {
hideWindow();
} else {
XRaiseWindow(dpy, win);
grabPluginKeyboard(dpy, win);
render();
}
} 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 < 5) {
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 < 5) {
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 + 5) % 6;
}
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) % 6;
}
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
};