// SPDX-License-Identifier: GPL-2.0-only // XDrawer - Lightweight X11 global phone controls drawer for Bananarchy (240x320) #include #include #include #include #include #include #ifndef _EVDEVK #define _EVDEVK(_v) (0x10081000 + (_v)) #endif #include #include #include #include "../sxmo-dwm/dwm_plugin.h" #include "../dwm-plugins/theme.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kScreenWidth = 240; constexpr int kScreenHeight = 320; constexpr int kTopBarHeight = 24; constexpr int kWinX = 0; constexpr int kWinY = kTopBarHeight; constexpr int kWinWidth = kScreenWidth; constexpr int kWinHeight = kScreenHeight - kTopBarHeight; // 296px flush below 24px dwm bar constexpr int kRowCount = 9; // Nerd Font PUA icons matching sxmo_hook_icons.sh & dwm ("Fira Code, DejaVuSansM Nerd Font") constexpr const char* kIconSpeaker = "\xef\x80\xa8"; // U+F028 speaker constexpr const char* kIconEarpiece = "\xef\x8a\xa0"; // U+F2A0 earpiece constexpr const char* kIconFlashlight = "\xf3\xb0\x89\x84"; // U+F0244 flashlight constexpr const char* kIconSoundOn = "\xf3\xb0\x8e\x88"; // U+F0388 music/sound on constexpr const char* kIconSoundOff = "\xef\x91\xa6"; // U+F466 mute constexpr const char* kIconMicOn = "\xf3\xb0\x8d\xac"; // U+F036C mic on constexpr const char* kIconMicOff = "\xf3\xb0\x8d\xad"; // U+F036D mic off constexpr const char* kIconBrightness = "\xf3\xb0\x83\x9d"; // U+F00DD brightness constexpr const char* kIconKeyboard = "\xef\x84\x9c"; // U+F11C keyboard constexpr const char* kIconBell = "\xef\x83\xb3"; // U+F0F3 bell / ringer constexpr const char* kIconBellSlash = "\xef\x87\xb6"; // U+F1F6 bell-slash constexpr const char* kIconToggleOn = "\xef\x88\x85"; // U+F205 toggle on constexpr const char* kIconToggleOff = "\xef\x88\x84"; // U+F204 toggle off volatile sig_atomic_t gRunning = 1; void handleSignal(int) { gRunning = 0; } int64_t nowMs() { struct timespec ts {}; clock_gettime(CLOCK_MONOTONIC, &ts); return static_cast(ts.tv_sec) * 1000 + ts.tv_nsec / 1000000; } std::string getRuntimeFile(const char* name) { const char* rdir = std::getenv("XDG_RUNTIME_DIR"); std::string base = (rdir && rdir[0]) ? rdir : "/tmp"; return base + "/" + name; } int readIntFile(const std::string& path, int fallback = -1) { int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); if (fd < 0) { return fallback; } char buf[64]; ssize_t n = ::read(fd, buf, sizeof(buf) - 1); ::close(fd); if (n <= 0) { return fallback; } buf[n] = '\0'; return std::atoi(buf); } bool writeIntFile(const std::string& path, int value) { int fd = ::open(path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC, 0644); if (fd < 0) { return false; } char buf[32]; int len = std::snprintf(buf, sizeof(buf), "%d\n", value); ssize_t w = ::write(fd, buf, len); ::close(fd); return w == len; } void runAsyncCmd(const std::string& cmd) { pid_t pid = ::fork(); if (pid == 0) { ::setsid(); int nullfd = ::open("/dev/null", O_RDWR); if (nullfd >= 0) { ::dup2(nullfd, STDIN_FILENO); ::dup2(nullfd, STDOUT_FILENO); ::dup2(nullfd, STDERR_FILENO); if (nullfd > 2) { ::close(nullfd); } } execl("/bin/sh", "sh", "-c", cmd.c_str(), static_cast(nullptr)); _exit(0); } } class AlsaControl { public: AlsaControl() { if (snd_ctl_open(&ctl_, "hw:0", 0) < 0) { ctl_ = nullptr; } } ~AlsaControl() { if (ctl_) { snd_ctl_close(ctl_); } } long getInteger(const char* name, long fallback = 0) const { if (!ctl_) { return fallback; } snd_ctl_elem_id_t* id = nullptr; snd_ctl_elem_value_t* val = nullptr; snd_ctl_elem_id_alloca(&id); snd_ctl_elem_value_alloca(&val); snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(id, name); snd_ctl_elem_value_set_id(val, id); if (snd_ctl_elem_read(ctl_, val) < 0) { return fallback; } return snd_ctl_elem_value_get_integer(val, 0); } bool setInteger(const char* name, long v) { if (!ctl_) { return false; } snd_ctl_elem_id_t* id = nullptr; snd_ctl_elem_value_t* val = nullptr; snd_ctl_elem_id_alloca(&id); snd_ctl_elem_value_alloca(&val); snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(id, name); snd_ctl_elem_value_set_id(val, id); snd_ctl_elem_value_set_integer(val, 0, v); return snd_ctl_elem_write(ctl_, val) >= 0; } bool getBoolean(const char* name, bool fallback = true) const { if (!ctl_) { return fallback; } snd_ctl_elem_id_t* id = nullptr; snd_ctl_elem_value_t* val = nullptr; snd_ctl_elem_id_alloca(&id); snd_ctl_elem_value_alloca(&val); snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(id, name); snd_ctl_elem_value_set_id(val, id); if (snd_ctl_elem_read(ctl_, val) < 0) { return fallback; } return snd_ctl_elem_value_get_boolean(val, 0) != 0; } bool setBoolean(const char* name, bool v) { if (!ctl_) { return false; } snd_ctl_elem_id_t* id = nullptr; snd_ctl_elem_value_t* val = nullptr; snd_ctl_elem_id_alloca(&id); snd_ctl_elem_value_alloca(&val); snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(id, name); snd_ctl_elem_value_set_id(val, id); snd_ctl_elem_value_set_boolean(val, 0, v ? 1 : 0); return snd_ctl_elem_write(ctl_, val) >= 0; } unsigned int getEnum(const char* name, unsigned int fallback = 0) const { if (!ctl_) { return fallback; } snd_ctl_elem_id_t* id = nullptr; snd_ctl_elem_value_t* val = nullptr; snd_ctl_elem_id_alloca(&id); snd_ctl_elem_value_alloca(&val); snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(id, name); snd_ctl_elem_value_set_id(val, id); if (snd_ctl_elem_read(ctl_, val) < 0) { return fallback; } return snd_ctl_elem_value_get_enumerated(val, 0); } bool setEnum(const char* name, unsigned int v) { if (!ctl_) { return false; } snd_ctl_elem_id_t* id = nullptr; snd_ctl_elem_value_t* val = nullptr; snd_ctl_elem_id_alloca(&id); snd_ctl_elem_value_alloca(&val); snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); snd_ctl_elem_id_set_name(id, name); snd_ctl_elem_value_set_id(val, id); snd_ctl_elem_value_set_enumerated(val, 0, v); return snd_ctl_elem_write(ctl_, val) >= 0; } private: snd_ctl_t* ctl_ = nullptr; }; enum class ControlId { SpeakerVol = 0, EarpieceVol = 1, RingVol = 2, RingerMode = 3, Flashlight = 4, SoundMute = 5, MicMute = 6, ScreenBright = 7, KbdBright = 8, }; struct ControlItem { ControlId id; const char* iconOn; const char* iconOff; std::string label; bool isSlider; int minVal; int maxVal; int stepVal; int value; }; class PhoneController { public: PhoneController() { if (::access("/sys/class/leds/white:torch/brightness", F_OK) == 0) { torchDir_ = "/sys/class/leds/white:torch"; } else { torchDir_ = "/sys/class/leds/white:flash"; } torchMax_ = std::max(1, readIntFile(torchDir_ + "/max_brightness", 255)); backlightDir_ = "/sys/class/backlight/backlight"; screenMax_ = std::max(1, readIntFile(backlightDir_ + "/max_brightness", 14)); kbdDir_ = "/sys/class/leds/white:kbd_backlight"; kbdMax_ = std::max(1, readIntFile(kbdDir_ + "/max_brightness", 1)); kbdSupportsSlider_ = (kbdMax_ > 1); refreshAll(); } void refreshAll() { items_.clear(); // 1. Speaker volume (0..100%, mapped to ALSA RX3 Digital Volume 60..92 + PipeWire 0..100%) int spkPct = readIntFile(getSxmoCfgDir() + "/speaker_vol", -1); if (spkPct < 0 || spkPct > 100) { long rx3 = alsa_.getInteger("RX3 Digital Volume", 92); if (rx3 <= 0) { spkPct = 0; } else if (rx3 >= 84) { spkPct = 100; } else { spkPct = std::clamp(static_cast(std::lround(((rx3 - 60) * 100.0) / 32.0)), 5, 100); } } spkPct = (spkPct / 5) * 5; items_.push_back({ControlId::SpeakerVol, kIconSpeaker, kIconSoundOff, "Speaker", true, 0, 100, 5, spkPct}); // 2. Earpiece volume (0..100%, mapped to ALSA RX1 Digital Volume 58..90) long rx1 = alsa_.getInteger("RX1 Digital Volume", 84); int earPct = 100; if (rx1 <= 0) { earPct = 0; } else if (rx1 < 84) { earPct = std::clamp(static_cast(std::lround((rx1 * 100.0) / 84.0)), 5, 100); } earPct = (earPct / 5) * 5; items_.push_back({ControlId::EarpieceVol, kIconEarpiece, kIconEarpiece, "Earpiece", true, 0, 100, 5, earPct}); // 3. Ringtone volume (0..100%, stored as SXMO_RINGVOL in ~/.config/sxmo/ringsettings) int ringPct = readRingVolume(); items_.push_back({ControlId::RingVol, kIconBell, kIconBellSlash, "Ring Vol", true, 0, 100, 5, ringPct}); // 4. Ringer Mode (0=Ring+Vib, 1=Vibrate, 2=Silent, 3=DND) int ringerVal = readRingerMode(); items_.push_back({ControlId::RingerMode, kIconBell, kIconBellSlash, "Ringer", false, 0, 3, 1, ringerVal}); // 5. Flashlight on/off (toggle) int torchCur = readIntFile(torchDir_ + "/brightness", 0); items_.push_back({ControlId::Flashlight, kIconFlashlight, kIconFlashlight, "Flashlight", false, 0, 1, 1, (torchCur > 0) ? 1 : 0}); // 6. Sound on/off (toggle) bool spkOn = alsa_.getBoolean("SPK DAC Switch", true); items_.push_back({ControlId::SoundMute, kIconSoundOn, kIconSoundOff, "Sound", false, 0, 1, 1, spkOn ? 1 : 0}); // 7. Microphone on/off (toggle) unsigned int dec1 = alsa_.getEnum("DEC1 MUX", 1); if (dec1 > 0) { savedMicMux_ = dec1; writeIntFile("/tmp/xdrawer_mic_mux", static_cast(savedMicMux_)); } else { savedMicMux_ = static_cast(std::max(1, readIntFile("/tmp/xdrawer_mic_mux", 1))); } items_.push_back({ControlId::MicMute, kIconMicOn, kIconMicOff, "Microphone", false, 0, 1, 1, (dec1 > 0) ? 1 : 0}); // 8. Screen brightness (slider 1..screenMax_) int scrCur = std::clamp(readIntFile(backlightDir_ + "/brightness", screenMax_), 1, screenMax_); int scrStep = std::max(1, screenMax_ / 14); items_.push_back({ControlId::ScreenBright, kIconBrightness, kIconBrightness, "Screen", true, 1, screenMax_, scrStep, scrCur}); // 9. Keyboard brightness (slider if max_brightness > 1, toggle otherwise) int kbdCur = std::clamp(readIntFile(kbdDir_ + "/brightness", 0), 0, kbdMax_); if (kbdSupportsSlider_) { int kbdStep = std::max(1, kbdMax_ / 10); items_.push_back({ControlId::KbdBright, kIconKeyboard, kIconKeyboard, "Keypad", true, 0, kbdMax_, kbdStep, kbdCur}); } else { items_.push_back({ControlId::KbdBright, kIconKeyboard, kIconKeyboard, "Keypad", false, 0, 1, 1, (kbdCur > 0) ? 1 : 0}); } } std::vector& items() { return items_; } const std::vector& items() const { return items_; } void adjust(size_t idx, int dir) { if (idx >= items_.size()) { return; } ControlItem& it = items_[idx]; if (it.isSlider) { it.value = std::clamp(it.value + dir * it.stepVal, it.minVal, it.maxVal); } else if (it.id == ControlId::RingerMode) { it.value = (it.value + dir + 4) % 4; } else { it.value = it.value ? 0 : 1; } apply(it); } void activate(size_t idx) { if (idx >= items_.size()) { return; } ControlItem& it = items_[idx]; if (it.id == ControlId::RingerMode) { it.value = (it.value + 1) % 4; apply(it); } else if (!it.isSlider) { it.value = it.value ? 0 : 1; apply(it); } } private: static std::string getSxmoCfgDir() { const char* xdg = std::getenv("XDG_CONFIG_HOME"); if (xdg && xdg[0]) { return std::string(xdg) + "/sxmo"; } const char* home = std::getenv("HOME"); return std::string((home && home[0]) ? home : "/home/user") + "/.config/sxmo"; } static int readRingerMode() { std::string busyFile = getRuntimeFile("sxmo.busy"); if (::access(busyFile.c_str(), F_OK) == 0) { return 3; // DND } std::string cfg = getSxmoCfgDir(); bool noRing = (::access((cfg + "/.noring").c_str(), F_OK) == 0); bool noVib = (::access((cfg + "/.novibrate").c_str(), F_OK) == 0); if (noRing && noVib) return 2; // Silent if (noRing) return 1; // Vibrate only return 0; // Ring + Vibrate } static int readRingVolume() { std::ifstream ifs(getSxmoCfgDir() + "/ringsettings"); std::string line; while (std::getline(ifs, line)) { auto eq = line.find('='); if (eq == std::string::npos) continue; std::string k = line.substr(0, eq); std::string v = line.substr(eq + 1); if (k == "SXMO_RINGVOL") { int val = std::clamp(std::atoi(v.c_str()), 0, 100); return (val / 5) * 5; } } return 100; } static void writeRingVolume(int ringVol) { std::string cfg = getSxmoCfgDir(); ::mkdir(cfg.c_str(), 0755); std::string path = cfg + "/ringsettings"; int ringTime = 20; int ringLoops = 10; int vibStrength = 2; { std::ifstream ifs(path); std::string line; while (std::getline(ifs, line)) { auto eq = line.find('='); if (eq == std::string::npos) continue; std::string k = line.substr(0, eq); int val = std::atoi(line.substr(eq + 1).c_str()); if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) ringTime = val; else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) ringLoops = val; else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) vibStrength = val; } } std::ofstream ofs(path, std::ios::trunc); if (ofs) { ofs << "SXMO_RINGTIME=" << ringTime << "\n"; ofs << "SXMO_RINGNUMBER=" << ringLoops << "\n"; ofs << "SXMO_VIBRATE_STRENGTH=" << vibStrength << "\n"; ofs << "SXMO_RINGVOL=" << std::clamp(ringVol, 0, 100) << "\n"; } } void apply(ControlItem& it) { switch (it.id) { case ControlId::SpeakerVol: { std::string cfg = getSxmoCfgDir(); ::mkdir(cfg.c_str(), 0755); writeIntFile(cfg + "/speaker_vol", it.value); long hw = (it.value <= 0) ? 0 : (60 + std::lround((it.value * 32.0) / 100.0)); if (it.value > 0) { alsa_.setBoolean("SPK DAC Switch", true); alsa_.setEnum("RX3 MIX1 INP1", 3); } alsa_.setInteger("RX3 Digital Volume", hw); char cmd[160]; std::snprintf( cmd, sizeof(cmd), "wpctl set-mute @DEFAULT_AUDIO_SINK@ %d >/dev/null 2>&1; " "wpctl set-volume @DEFAULT_AUDIO_SINK@ %d%% >/dev/null 2>&1; " "sxmo_hook_statusbar.sh volume >/dev/null 2>&1", (it.value <= 0) ? 1 : 0, it.value); runAsyncCmd(cmd); break; } case ControlId::EarpieceVol: { long hw = (it.value <= 0) ? 0 : (58 + std::lround((it.value * 32.0) / 100.0)); alsa_.setInteger("RX1 Digital Volume", hw); break; } case ControlId::RingVol: { writeRingVolume(it.value); break; } case ControlId::Flashlight: { int v = it.value ? torchMax_ : 0; if (!writeIntFile(torchDir_ + "/brightness", v)) { runAsyncCmd(it.value ? "brightnessctl -d white:torch set 255" : "brightnessctl -d white:torch set 0"); } break; } case ControlId::SoundMute: { alsa_.setBoolean("SPK DAC Switch", it.value != 0); if (it.value != 0) { alsa_.setEnum("RX3 MIX1 INP1", 3); } runAsyncCmd(it.value ? "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1; " "sxmo_hook_statusbar.sh audio >/dev/null 2>&1" : "wpctl set-mute @DEFAULT_AUDIO_SINK@ 1 >/dev/null 2>&1; " "sxmo_hook_statusbar.sh audio >/dev/null 2>&1"); break; } case ControlId::MicMute: { alsa_.setEnum("DEC1 MUX", it.value ? (savedMicMux_ > 0 ? savedMicMux_ : 1) : 0); alsa_.setInteger("ADC1 Volume", it.value ? 6 : 0); alsa_.setInteger("ADC3 Volume", it.value ? 6 : 0); runAsyncCmd(it.value ? "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0 >/dev/null 2>&1" : "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 1 >/dev/null 2>&1"); break; } case ControlId::RingerMode: { std::string cfg = getSxmoCfgDir(); ::mkdir(cfg.c_str(), 0755); std::string noRingPath = cfg + "/.noring"; std::string noVibPath = cfg + "/.novibrate"; std::string busyPath = getRuntimeFile("sxmo.busy"); if (it.value == 0) { // Ring + Vibrate ::unlink(noRingPath.c_str()); ::unlink(noVibPath.c_str()); ::unlink(busyPath.c_str()); } else if (it.value == 1) { // Vibrate only writeIntFile(noRingPath, 1); ::unlink(noVibPath.c_str()); ::unlink(busyPath.c_str()); } else if (it.value == 2) { // Silent writeIntFile(noRingPath, 1); writeIntFile(noVibPath, 1); ::unlink(busyPath.c_str()); } else { // DND writeIntFile(busyPath, 1); } runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1"); break; } case ControlId::ScreenBright: { writeIntFile(backlightDir_ + "/bl_power", 0); writeIntFile(backlightDir_ + "/brightness", it.value); writeIntFile("/tmp/sxmo_saved_brightness", it.value); break; } case ControlId::KbdBright: { int v = kbdSupportsSlider_ ? it.value : (it.value ? kbdMax_ : 0); if (!writeIntFile(kbdDir_ + "/brightness", v)) { char cmd[128]; std::snprintf(cmd, sizeof(cmd), "brightnessctl -d white:kbd_backlight set %d >/dev/null 2>&1", v); runAsyncCmd(cmd); } break; } } } AlsaControl alsa_; std::string torchDir_; int torchMax_ = 255; std::string backlightDir_; int screenMax_ = 14; std::string kbdDir_; int kbdMax_ = 1; bool kbdSupportsSlider_ = false; unsigned int savedMicMux_ = 2; std::vector items_; }; } // namespace static int xdrawer_run_main(int argc, char** argv) { gRunning = 1; bool isPreload = false; for (int i = 1; i < argc; ++i) { if (argv[i] && std::strcmp(argv[i], "--preload") == 0) { isPreload = true; } } std::string daemonPidPath = "/tmp/dwm_plugin_xdrawer.pid"; std::string fifoPath = "/tmp/dwm_plugin_xdrawer.fifo"; std::string pidPath = getRuntimeFile("xdrawer.pid"); int existingDaemon = readIntFile(daemonPidPath, -1); bool daemonAlive = false; if (existingDaemon > 0 && existingDaemon != static_cast(::getpid()) && ::kill(existingDaemon, 0) == 0) { char cmdPath[64]; std::snprintf(cmdPath, sizeof(cmdPath), "/proc/%d/cmdline", existingDaemon); int cfd = ::open(cmdPath, O_RDONLY | O_CLOEXEC); if (cfd >= 0) { char cbuf[256]; ssize_t cn = ::read(cfd, cbuf, sizeof(cbuf) - 1); ::close(cfd); if (cn > 0) { for (ssize_t i = 0; i < cn; ++i) { if (cbuf[i] == '\0') cbuf[i] = ' '; } cbuf[cn] = '\0'; if (std::strstr(cbuf, "xdrawer") != nullptr) { daemonAlive = true; } } } } if (daemonAlive) { if (!isPreload) { int ffd = ::open(fifoPath.c_str(), O_WRONLY | O_NONBLOCK | O_CLOEXEC); if (ffd >= 0) { (void)::write(ffd, "TOGGLE\n", 7); ::close(ffd); } } return 0; } writeIntFile(daemonPidPath, static_cast(::getpid())); ::mkfifo(fifoPath.c_str(), 0666); ::chmod(fifoPath.c_str(), 0666); int fifoFd = ::open(fifoPath.c_str(), O_RDWR | O_NONBLOCK | O_CLOEXEC); struct sigaction sa {}; sa.sa_handler = handleSignal; sigemptyset(&sa.sa_mask); sigaction(SIGTERM, &sa, nullptr); sigaction(SIGINT, &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); XSetWindowAttributes swa{}; swa.override_redirect = True; swa.background_pixel = 0x222222; swa.border_pixel = 0xf4d03f; swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | StructureNotifyMask | VisibilityChangeMask; Window win = XCreateWindow( dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0, DefaultDepth(dpy, screen), CopyFromParent, visual, CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); XClassHint ch{}; char resName[] = "xdrawer"; char resClass[] = "XDrawer"; ch.res_name = resName; ch.res_class = resClass; XSetClassHint(dpy, win, &ch); XStoreName(dpy, win, "XDrawer"); Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight, DefaultDepth(dpy, screen)); GC gc = XCreateGC(dpy, pix, 0, nullptr); XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap); PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen); PangoContext* pangoCtx = pango_font_map_create_context(fontMap); PangoLayout* layoutMain = pango_layout_new(pangoCtx); PangoLayout* layoutIcon = pango_layout_new(pangoCtx); PangoLayout* layoutSmall = pango_layout_new(pangoCtx); bananarchy::PluginTheme theme; theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, nullptr, win); XftColor& colWhite = theme.colWhite; XftColor& colLight = theme.colLight; XftColor& colMuted = theme.colMuted; XftColor& colYellow = theme.colYellow; XftColor& colGreen = theme.colGreen; PhoneController controller; size_t selected = 0; bool visible = false; int64_t openStartMs = 0; int64_t startupIgnoreUntilMs = 0; bool initialKeysReleased = true; auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h, const std::string& text, int maxW = -1) { pango_layout_set_text(layout, text.c_str(), static_cast(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); } PangoRectangle ink{}, logical{}; pango_layout_get_pixel_extents(layout, &ink, &logical); int ty = y + (h - logical.height) / 2; pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE, ty * PANGO_SCALE); }; auto pangoWidth = [&](PangoLayout* layout, const std::string& text) -> int { pango_layout_set_width(layout, -1); pango_layout_set_text(layout, text.c_str(), static_cast(text.size())); PangoRectangle ink{}, logical{}; pango_layout_get_pixel_extents(layout, &ink, &logical); return logical.width; }; auto render = [&]() { XSetForeground(dpy, gc, theme.spec.bgMain); XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight); const auto& items = controller.items(); for (size_t i = 0; i < items.size(); ++i) { int ry = static_cast((i * kWinHeight) / kRowCount); int nextRy = static_cast(((i + 1) * kWinHeight) / kRowCount); int rowH = nextRy - ry; bool isSel = (i == selected); XSetForeground(dpy, gc, isSel ? theme.spec.selBg : ((i % 2 == 0) ? theme.spec.bgAltEven : theme.spec.bgAltOdd)); XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH); if (isSel) { XSetForeground(dpy, gc, theme.spec.accent); XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH); } const ControlItem& it = items[i]; bool activeState = (it.id == ControlId::RingerMode) ? (it.value == 0 || it.value == 1) : (it.value > 0); const char* rowIcon = activeState ? it.iconOn : it.iconOff; int labelMaxW = it.isSlider ? 82 : 126; drawPango(layoutIcon, isSel ? &colYellow : &colLight, 8, ry, rowH, rowIcon); drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry, rowH, it.label, labelMaxW); if (it.isSlider) { int pct = 0; if (it.maxVal > it.minVal) { if (it.minVal == 1) { pct = static_cast(std::lround((it.value * 100.0) / it.maxVal)); } else { pct = static_cast( std::lround(((it.value - it.minVal) * 100.0) / (it.maxVal - it.minVal))); } } pct = std::clamp(pct, 0, 100); int barX = 114; int barW = 78; int barH = 12; int barY = ry + (rowH - barH) / 2; XSetForeground(dpy, gc, theme.spec.bgOverlay); XFillRectangle(dpy, pix, gc, barX, barY, barW, barH); XSetForeground(dpy, gc, isSel ? theme.spec.fgMain : theme.spec.border); XDrawRectangle(dpy, pix, gc, barX, barY, barW - 1, barH - 1); int fillW = ((barW - 2) * pct) / 100; if (fillW > 0) { XSetForeground(dpy, gc, isSel ? theme.spec.accent : theme.spec.info); XFillRectangle(dpy, pix, gc, barX + 1, barY + 1, fillW, barH - 2); } int knobX = barX + std::clamp(fillW - 2, 0, barW - 5); XSetForeground(dpy, gc, theme.spec.fgBright); XFillRectangle(dpy, pix, gc, knobX, barY - 2, 5, barH + 4); char pctBuf[16]; std::snprintf(pctBuf, sizeof(pctBuf), "%d%%", pct); std::string pctStr(pctBuf); int pw = pangoWidth(layoutSmall, pctStr); drawPango(layoutSmall, isSel ? &colWhite : &colLight, kWinWidth - pw - 6, ry, rowH, pctStr); } else if (it.id == ControlId::RingerMode) { const char* rModes[4] = {"Ring+Vib", "Vibrate", "Silent", "DND"}; std::string stStr = std::string("< ") + rModes[std::clamp(it.value, 0, 3)] + " >"; int stW = pangoWidth(layoutSmall, stStr); XftColor* rCol = (it.value == 0) ? (isSel ? &colYellow : &colGreen) : ((it.value == 1) ? &colYellow : &colMuted); drawPango(layoutSmall, rCol, kWinWidth - stW - 6, ry, rowH, stStr); } else { bool on = (it.value != 0); std::string stStr = on ? "ON" : "OFF"; int stW = pangoWidth(layoutMain, stStr); drawPango(layoutMain, on ? (isSel ? &colYellow : &colGreen) : &colMuted, kWinWidth - stW - 8, ry, rowH, stStr); drawPango(layoutIcon, on ? (isSel ? &colYellow : &colGreen) : &colMuted, kWinWidth - stW - 30, ry, rowH, on ? kIconToggleOn : kIconToggleOff); } } if (visible) { XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); XFlush(dpy); } }; // Pre-warm Pango/FreeType/Xrender glyph caches into offscreen Pixmap at startup render(); XSync(dpy, False); auto writeSoftkeySuppress = [](int64_t untilMs) { int fd = ::open("/tmp/sxmo_softkey_suppress", O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); if (fd >= 0) { char buf[32]; int len = std::snprintf(buf, sizeof(buf), "%lld\n", static_cast(untilMs)); (void)::write(fd, buf, len); ::close(fd); } }; auto querySoftkeysDown = [&](Display* d) -> int { char km[32]; if (!XQueryKeymap(d, km)) { return 0; } int mask = 0; if (km[135 / 8] & (1 << (135 % 8))) { mask |= 1; } if (km[166 / 8] & (1 << (166 % 8))) { mask |= 2; } return mask; }; int xfd = ConnectionNumber(dpy); auto closeOtherPlugin = [](const char* activePidName, const char* fifoPathStr) { if (readIntFile(getRuntimeFile(activePidName), -1) > 0) { int ffd = ::open(fifoPathStr, O_WRONLY | O_NONBLOCK | O_CLOEXEC); if (ffd >= 0) { (void)::write(ffd, "CLOSE\n", 6); ::close(ffd); } } }; auto isOtherPluginActive = [&]() { static const char* kPids[] = { "xdialer.pid", "xcalllog.pid", "xcallscreen.pid", "xcontacts.pid", "xsms.pid", "xfiles.pid", "xcamera.pid", "xthemes.pid", "xmodem.pid", "xnetworks.pid", "xringtones.pid", "xwallpapers.pid", }; for (const char* pf : kPids) { int p = readIntFile(getRuntimeFile(pf), -1); if (p > 0 && ::kill(p, 0) == 0) { return true; } } if (::access("/tmp/dwm_plugin_dmenu.active", F_OK) == 0) { return true; } return false; }; auto showWindow = [&]() { if (visible) { return; } writeIntFile(pidPath, static_cast(::getpid())); closeOtherPlugin("xdialer.pid", "/tmp/dwm_plugin_dialer.fifo"); closeOtherPlugin("xcalllog.pid", "/tmp/dwm_plugin_calllog.fifo"); closeOtherPlugin("xcallscreen.pid", "/tmp/dwm_plugin_callscreen.fifo"); closeOtherPlugin("xcontacts.pid", "/tmp/dwm_plugin_contacts.fifo"); closeOtherPlugin("xsms.pid", "/tmp/dwm_plugin_sms.fifo"); closeOtherPlugin("xfiles.pid", "/tmp/dwm_plugin_files.fifo"); closeOtherPlugin("xcamera.pid", "/tmp/dwm_plugin_camera.fifo"); closeOtherPlugin("xthemes.pid", "/tmp/dwm_plugin_themes.fifo"); closeOtherPlugin("xmodem.pid", "/tmp/dwm_plugin_modem.fifo"); closeOtherPlugin("xnetworks.pid", "/tmp/dwm_plugin_networks.fifo"); closeOtherPlugin("xringtones.pid", "/tmp/dwm_plugin_ringtones.fifo"); closeOtherPlugin("xwallpapers.pid", "/tmp/dwm_plugin_wallpapers.fifo"); if (::access("/tmp/dwm_plugin_dmenu.active", F_OK) == 0) { int dfd = ::open("/tmp/dwm_plugin_dmenu.fifo", O_WRONLY | O_NONBLOCK | O_CLOEXEC); if (dfd >= 0) { (void)::write(dfd, "CLOSE\n", 6); ::close(dfd); } } theme.checkAndReload(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, nullptr, win); openStartMs = nowMs(); startupIgnoreUntilMs = openStartMs + 180; initialKeysReleased = (querySoftkeysDown(dpy) == 0); visible = true; XMapRaised(dpy, win); XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0); for (int i = 0; i < 40; ++i) { if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) == GrabSuccess) { break; } usleep(3000); } XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); XFlush(dpy); controller.refreshAll(); render(); }; auto hideWindow = [&](bool drainSoftkeys) { if (!visible) { return; } if (drainSoftkeys) { int64_t drainDeadline = nowMs() + 220; while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) { while (XPending(dpy)) { XEvent ev; XNextEvent(dpy, &ev); } struct pollfd pfd {}; pfd.fd = xfd; pfd.events = POLLIN; ::poll(&pfd, 1, 10); } writeSoftkeySuppress(nowMs() + 350); } XUngrabKeyboard(dpy, CurrentTime); XUnmapWindow(dpy, win); XFlush(dpy); ::unlink(pidPath.c_str()); visible = false; }; if (!isPreload) { showWindow(); } while (gRunning) { if (fifoFd >= 0) { char fbuf[256]; ssize_t fn = ::read(fifoFd, fbuf, sizeof(fbuf) - 1); if (fn > 0) { fbuf[fn] = '\0'; std::stringstream ss(fbuf); std::string cmd; while (std::getline(ss, cmd)) { if (!cmd.empty() && cmd.back() == '\r') cmd.pop_back(); if (cmd == "TOGGLE") { if (!visible) { showWindow(); } else if (nowMs() - openStartMs >= 350) { hideWindow(true); } } else if (cmd == "OPEN") { showWindow(); } else if (cmd == "CLOSE") { hideWindow(false); } else if (cmd == "RELOAD_THEME") { theme.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon, nullptr, win); render(); } } } } if (visible && !initialKeysReleased && querySoftkeysDown(dpy) == 0) { initialKeysReleased = true; } while (XPending(dpy)) { XEvent ev; XNextEvent(dpy, &ev); if (!visible) { continue; } if (!initialKeysReleased && querySoftkeysDown(dpy) == 0) { initialKeysReleased = true; } if (ev.type == Expose) { if (ev.xexpose.count == 0) { render(); } } else if (ev.type == VisibilityNotify && ev.xvisibility.state != VisibilityUnobscured) { if (isOtherPluginActive()) { hideWindow(false); } else { XRaiseWindow(dpy, win); XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime); XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime); render(); } } else if (ev.type == KeyRelease) { KeySym sym = XLookupKeysym(&ev.xkey, 0); if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || ev.xkey.keycode == 135 || ev.xkey.keycode == 166) { if (querySoftkeysDown(dpy) == 0) { initialKeysReleased = true; } } } else if (ev.type == KeyPress) { KeySym sym = XLookupKeysym(&ev.xkey, 0); size_t count = controller.items().size(); if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 || ev.xkey.keycode == 135 || ev.xkey.keycode == 166) { if (initialKeysReleased && nowMs() >= startupIgnoreUntilMs) { writeSoftkeySuppress(nowMs() + 450); hideWindow(true); break; } continue; } switch (sym) { case XK_Escape: case XK_BackSpace: case XK_q: writeSoftkeySuppress(nowMs() + 350); hideWindow(true); break; case XK_Up: case XK_KP_Up: case XK_k: if (count > 0) { selected = (selected + count - 1) % count; render(); } break; case XK_Down: case XK_KP_Down: case XK_j: if (count > 0) { selected = (selected + 1) % count; render(); } break; case XK_Left: case XK_KP_Left: case XK_h: case XF86XK_AudioLowerVolume: controller.adjust(selected, -1); render(); break; case XK_Right: case XK_KP_Right: case XK_l: case XF86XK_AudioRaiseVolume: controller.adjust(selected, +1); render(); break; case XK_Return: case XK_KP_Enter: case XK_space: case XF86XK_Phone: case XF86XK_PickupPhone: controller.activate(selected); render(); break; case XK_1: case XK_2: case XK_3: case XK_4: case XK_5: case XK_6: case XK_7: case XK_8: case XK_9: { size_t idx = static_cast(sym - XK_1); if (idx < count) { selected = idx; controller.activate(selected); render(); } break; } default: break; } } } int timeoutMs = visible ? (initialKeysReleased ? 250 : 40) : -1; struct pollfd pfds[2] {}; pfds[0].fd = xfd; pfds[0].events = POLLIN; pfds[1].fd = fifoFd; pfds[1].events = POLLIN; ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs); } hideWindow(false); if (fifoFd >= 0) ::close(fifoFd); ::unlink(fifoPath.c_str()); ::unlink(daemonPidPath.c_str()); g_object_unref(layoutSmall); g_object_unref(layoutIcon); g_object_unref(layoutMain); g_object_unref(pangoCtx); XftDrawDestroy(xftDraw); XFreeGC(dpy, gc); XFreePixmap(dpy, pix); XDestroyWindow(dpy, win); XCloseDisplay(dpy); return 0; } static int xdrawer_plugin_init(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("xdrawer", "--preload"); } return 0; } static int xdrawer_on_softkey_chord(const DwmHostApi* host) { if (host && host->spawn_plugin) { host->spawn_plugin("xdrawer", nullptr); return 1; } return 0; } extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = { DWM_PLUGIN_API_VERSION, "xdrawer", "Global phone hardware controls drawer (softkey chord)", xdrawer_plugin_init, nullptr, xdrawer_on_softkey_chord, nullptr, nullptr, xdrawer_run_main }; int main(int argc, char** argv) { return xdrawer_run_main(argc, argv); }