1152 lines
37 KiB
C++
1152 lines
37 KiB
C++
// SPDX-License-Identifier: GPL-2.0-only
|
|
// XDrawer - Lightweight X11 global phone controls drawer for Bananarchy (240x320)
|
|
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xft/Xft.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/keysym.h>
|
|
#include <X11/XF86keysym.h>
|
|
#ifndef _EVDEVK
|
|
#define _EVDEVK(_v) (0x10081000 + (_v))
|
|
#endif
|
|
#include <alsa/asoundlib.h>
|
|
#include <pango/pango.h>
|
|
#include <pango/pangoxft.h>
|
|
|
|
#include "../sxmo-dwm/dwm_plugin.h"
|
|
#include "../dwm-plugins/theme.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <csignal>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <fcntl.h>
|
|
#include <fstream>
|
|
#include <poll.h>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
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<int64_t>(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<char*>(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<int>(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<int>(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<int>(savedMicMux_));
|
|
} else {
|
|
savedMicMux_ = static_cast<unsigned int>(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<ControlItem>& items() { return items_; }
|
|
const std::vector<ControlItem>& 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<ControlItem> 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<int>(::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<int>(::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<int>(text.size()));
|
|
if (maxW > 0) {
|
|
pango_layout_set_width(layout, maxW * PANGO_SCALE);
|
|
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
|
|
} else {
|
|
pango_layout_set_width(layout, -1);
|
|
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
|
|
}
|
|
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<int>(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<int>((i * kWinHeight) / kRowCount);
|
|
int nextRy = static_cast<int>(((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<int>(std::lround((it.value * 100.0) / it.maxVal));
|
|
} else {
|
|
pct = static_cast<int>(
|
|
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<long long>(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<int>(::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<size_t>(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);
|
|
}
|