// 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 #include #include #include #include #include #include #include #include #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(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0])); constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50}; constexpr int kNumRingLoopSteps = static_cast(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(std::toupper(static_cast(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& out) { DIR* d = ::opendir(dirPath.c_str()); if (!d) return; std::vector 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 discoverRingtones() { std::vector 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(::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 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(tones.size())) { selIdx = std::max(0, static_cast(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(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(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(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(::getpid())); closeOtherPlugins("ringtones"); refreshData(); for (size_t i = 0; i < tones.size(); ++i) { if (tones[i].path == activePath) { selIdx = static_cast(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(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(tones.size()) - 1) % static_cast(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(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(rem); } } if (!statusBanner.empty()) { int64_t bRem = statusBannerUntilMs - now; if (bRem <= 0) { statusBanner.clear(); render(); } else if (timeoutMs < 0 || bRem < timeoutMs) { timeoutMs = static_cast(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 };