1381 lines
46 KiB
C++
1381 lines
46 KiB
C++
#include "phone_data.h"
|
|||
|
|
#include "../sxmo-dwm/dwm_plugin.h"
|
||
|
|
|
||
|
|
#include <X11/Xatom.h>
|
||
|
|
#include <X11/Xft/Xft.h>
|
||
|
|
#include <X11/Xutil.h>
|
||
|
|
#include <pango/pangoxft.h>
|
||
|
|
#include <poll.h>
|
||
|
|
#include <sys/stat.h>
|
||
|
|
#include <sys/wait.h>
|
||
|
|
#include <clocale>
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
using namespace bananarchy;
|
||
|
|
|
||
|
|
volatile sig_atomic_t gRunning = 1;
|
||
|
|
|
||
|
|
void handleSignal(int) {
|
||
|
|
gRunning = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
enum class ViewMode {
|
||
|
|
Browser = 0,
|
||
|
|
Actions = 1,
|
||
|
|
Customize = 2,
|
||
|
|
HexModal = 3,
|
||
|
|
NameModal = 4,
|
||
|
|
ConfirmDelete = 5,
|
||
|
|
};
|
||
|
|
|
||
|
|
enum class NameModalAction {
|
||
|
|
SaveCurrentAs = 0,
|
||
|
|
DuplicateTheme = 1,
|
||
|
|
};
|
||
|
|
|
||
|
|
enum class TextCaseMode {
|
||
|
|
Title = 0, // Abc
|
||
|
|
Lower = 1, // abc
|
||
|
|
Upper = 2, // ABC
|
||
|
|
Num = 3, // 123
|
||
|
|
};
|
||
|
|
|
||
|
|
const char* kMultiTapTheme[10] = {
|
||
|
|
" _-0", // 0
|
||
|
|
".-_1", // 1
|
||
|
|
"abc2", // 2
|
||
|
|
"def3", // 3
|
||
|
|
"ghi4", // 4
|
||
|
|
"jkl5", // 5
|
||
|
|
"mno6", // 6
|
||
|
|
"pqrs7", // 7
|
||
|
|
"tuv8", // 8
|
||
|
|
"wxyz9" // 9
|
||
|
|
};
|
||
|
|
|
||
|
|
const char* kHexKeyMap[10] = {
|
||
|
|
"0", // 0
|
||
|
|
"1", // 1
|
||
|
|
"2abc", // 2
|
||
|
|
"3def", // 3
|
||
|
|
"4", // 4
|
||
|
|
"5", // 5
|
||
|
|
"6", // 6
|
||
|
|
"7", // 7
|
||
|
|
"8", // 8
|
||
|
|
"9" // 9
|
||
|
|
};
|
||
|
|
|
||
|
|
const char* kFontPresets[] = {
|
||
|
|
"Fira Code",
|
||
|
|
"DejaVuSansM Nerd Font",
|
||
|
|
"DejaVu Sans Mono",
|
||
|
|
"DejaVu Sans",
|
||
|
|
"monospace",
|
||
|
|
"sans-serif"
|
||
|
|
};
|
||
|
|
constexpr int kNumFontPresets =
|
||
|
|
static_cast<int>(sizeof(kFontPresets) / sizeof(kFontPresets[0]));
|
||
|
|
|
||
|
|
const char* kColorPresets[] = {
|
||
|
|
"#222222", "#181818", "#141414", "#101418", "#0d1117", "#1e1e2e",
|
||
|
|
"#282a36", "#2e3440", "#002b36", "#1d2021", "#2b1b17", "#121b18",
|
||
|
|
"#f5f5f0", "#ebdbb2", "#ffffff", "#dddddd", "#bbbbbb", "#999999",
|
||
|
|
"#005577", "#0088bb", "#1f6feb", "#3d59a1", "#6272a4", "#5e81ac",
|
||
|
|
"#b57614", "#2ecc71", "#27ae60", "#8ec07c", "#a6e3a1", "#5dade2",
|
||
|
|
"#89b4fa", "#88c0d0", "#f4d03f", "#fabd2f", "#f9e2af", "#e67e22",
|
||
|
|
"#fe8019", "#e74c3c", "#fb4934", "#f38ba8", "#bd93f9", "#cba6f7"
|
||
|
|
};
|
||
|
|
constexpr int kNumColorPresets =
|
||
|
|
static_cast<int>(sizeof(kColorPresets) / sizeof(kColorPresets[0]));
|
||
|
|
|
||
|
|
struct ThemeFileEntry {
|
||
|
|
std::string name; // basename without .xth
|
||
|
|
std::string fullPath; // ~/.xthemes/<name>.xth
|
||
|
|
ThemeSpec spec;
|
||
|
|
bool isActive = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
void popUtf8(std::string& s) {
|
||
|
|
if (s.empty()) return;
|
||
|
|
while (!s.empty() && (static_cast<unsigned char>(s.back()) & 0xC0) == 0x80) {
|
||
|
|
s.pop_back();
|
||
|
|
}
|
||
|
|
if (!s.empty()) s.pop_back();
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string getXthemesDir() {
|
||
|
|
const char* home = std::getenv("HOME");
|
||
|
|
std::string dir =
|
||
|
|
std::string((home && home[0]) ? home : "/home/user") + "/.xthemes";
|
||
|
|
::mkdir(dir.c_str(), 0755);
|
||
|
|
return dir;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool writeTextFile(const std::string& path, const std::string& content) {
|
||
|
|
ContactBook::ensureParentDir(path);
|
||
|
|
std::string tmpPath = path + ".tmp";
|
||
|
|
{
|
||
|
|
std::ofstream out(tmpPath, std::ios::trunc);
|
||
|
|
if (!out.is_open()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
out << content;
|
||
|
|
out.flush();
|
||
|
|
if (!out.good()) {
|
||
|
|
::unlink(tmpPath.c_str());
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ::rename(tmpPath.c_str(), path.c_str()) == 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
void updateRootResourceManager(const std::string& xresContent,
|
||
|
|
Display* existingDpy = nullptr) {
|
||
|
|
Display* dpy = existingDpy ? existingDpy : XOpenDisplay(nullptr);
|
||
|
|
if (!dpy) return;
|
||
|
|
Window root = DefaultRootWindow(dpy);
|
||
|
|
XChangeProperty(dpy, root, XA_RESOURCE_MANAGER, XA_STRING, 8, PropModeReplace,
|
||
|
|
reinterpret_cast<const unsigned char*>(xresContent.data()),
|
||
|
|
static_cast<int>(xresContent.size()));
|
||
|
|
XFlush(dpy);
|
||
|
|
if (!existingDpy) {
|
||
|
|
XCloseDisplay(dpy);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void ensureDefaultThemesSeeded() {
|
||
|
|
std::string dir = getXthemesDir();
|
||
|
|
std::string defPath = dir + "/Default.xth";
|
||
|
|
if (::access(defPath.c_str(), R_OK) != 0) {
|
||
|
|
if (::access("/etc/skel/.xthemes/Default.xth", R_OK) == 0) {
|
||
|
|
ThemeSpec skelSpec;
|
||
|
|
skelSpec.name = "Default";
|
||
|
|
skelSpec.loadFromFile("/etc/skel/.xthemes/Default.xth");
|
||
|
|
writeTextFile(defPath, skelSpec.serializeXresources());
|
||
|
|
} else {
|
||
|
|
ThemeSpec defSpec;
|
||
|
|
defSpec.name = "Default";
|
||
|
|
writeTextFile(defPath, defSpec.serializeXresources());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Also seed any additional prepackaged themes from /etc/skel/.xthemes/
|
||
|
|
DIR* skelD = ::opendir("/etc/skel/.xthemes");
|
||
|
|
if (skelD) {
|
||
|
|
struct dirent* ent;
|
||
|
|
while ((ent = ::readdir(skelD)) != nullptr) {
|
||
|
|
std::string fname = ent->d_name;
|
||
|
|
if (fname.size() > 4 && fname.substr(fname.size() - 4) == ".xth") {
|
||
|
|
std::string target = dir + "/" + fname;
|
||
|
|
if (::access(target.c_str(), R_OK) != 0) {
|
||
|
|
ThemeSpec ts;
|
||
|
|
ts.name = fname.substr(0, fname.size() - 4);
|
||
|
|
if (ts.loadFromFile("/etc/skel/.xthemes/" + fname)) {
|
||
|
|
ts.name = fname.substr(0, fname.size() - 4);
|
||
|
|
writeTextFile(target, ts.serializeXresources());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
::closedir(skelD);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string xresPath = getXresourcesPath();
|
||
|
|
if (::access(xresPath.c_str(), R_OK) != 0) {
|
||
|
|
ThemeSpec defSpec;
|
||
|
|
defSpec.loadFromFile(defPath);
|
||
|
|
writeTextFile(xresPath, defSpec.serializeXresources());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool applyThemeSpecSystemWide(const ThemeSpec& spec,
|
||
|
|
Display* existingDpy = nullptr,
|
||
|
|
const char* skipPlugin = "") {
|
||
|
|
std::string xresPath = getXresourcesPath();
|
||
|
|
std::string content = spec.serializeXresources();
|
||
|
|
if (!writeTextFile(xresPath, content)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
updateRootResourceManager(content, existingDpy);
|
||
|
|
broadcastThemeReload(skipPlugin ? skipPlugin : "");
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<ThemeFileEntry> loadInstalledThemes(const ThemeSpec& activeSpec) {
|
||
|
|
ensureDefaultThemesSeeded();
|
||
|
|
std::vector<ThemeFileEntry> list;
|
||
|
|
std::string dir = getXthemesDir();
|
||
|
|
DIR* d = ::opendir(dir.c_str());
|
||
|
|
if (!d) return list;
|
||
|
|
|
||
|
|
struct dirent* ent;
|
||
|
|
while ((ent = ::readdir(d)) != nullptr) {
|
||
|
|
std::string fname = ent->d_name;
|
||
|
|
if (fname.size() <= 4 || fname.substr(fname.size() - 4) != ".xth") {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
ThemeFileEntry item;
|
||
|
|
item.name = fname.substr(0, fname.size() - 4);
|
||
|
|
item.fullPath = dir + "/" + fname;
|
||
|
|
item.spec.name = item.name;
|
||
|
|
item.spec.loadFromFile(item.fullPath);
|
||
|
|
item.spec.name = item.name;
|
||
|
|
|
||
|
|
bool nameMatch =
|
||
|
|
(toLowerStr(activeSpec.name) == toLowerStr(item.name));
|
||
|
|
bool colorMatch =
|
||
|
|
(activeSpec.bgMain == item.spec.bgMain &&
|
||
|
|
activeSpec.selBg == item.spec.selBg &&
|
||
|
|
activeSpec.accent == item.spec.accent &&
|
||
|
|
activeSpec.fgMain == item.spec.fgMain &&
|
||
|
|
activeSpec.baseFontSize == item.spec.baseFontSize &&
|
||
|
|
toLowerStr(activeSpec.fontFace) == toLowerStr(item.spec.fontFace));
|
||
|
|
item.isActive = nameMatch || colorMatch;
|
||
|
|
list.push_back(item);
|
||
|
|
}
|
||
|
|
::closedir(d);
|
||
|
|
|
||
|
|
std::sort(list.begin(), list.end(),
|
||
|
|
[](const ThemeFileEntry& a, const ThemeFileEntry& b) {
|
||
|
|
if (a.name == "Default" && b.name != "Default") return true;
|
||
|
|
if (b.name == "Default" && a.name != "Default") return false;
|
||
|
|
return toLowerStr(a.name) < toLowerStr(b.name);
|
||
|
|
});
|
||
|
|
return list;
|
||
|
|
}
|
||
|
|
|
||
|
|
int runThemesApp(int argc, char** argv) {
|
||
|
|
gRunning = 1;
|
||
|
|
std::setlocale(LC_ALL, "");
|
||
|
|
XSetLocaleModifiers("");
|
||
|
|
|
||
|
|
bool isPreload = false;
|
||
|
|
std::string applyArg;
|
||
|
|
bool exportDefaultOnly = false;
|
||
|
|
std::string openArg;
|
||
|
|
|
||
|
|
for (int i = 1; i < argc; ++i) {
|
||
|
|
if (!argv[i]) continue;
|
||
|
|
if (std::strcmp(argv[i], "--preload") == 0) {
|
||
|
|
isPreload = true;
|
||
|
|
} else if (std::strcmp(argv[i], "--export-default") == 0) {
|
||
|
|
exportDefaultOnly = true;
|
||
|
|
} else if (std::strcmp(argv[i], "--apply") == 0 && i + 1 < argc &&
|
||
|
|
argv[i + 1]) {
|
||
|
|
applyArg = argv[++i];
|
||
|
|
} else if (argv[i][0] != '-' && openArg.empty()) {
|
||
|
|
openArg = argv[i];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ensureDefaultThemesSeeded();
|
||
|
|
|
||
|
|
if (exportDefaultOnly) {
|
||
|
|
ThemeSpec defSpec;
|
||
|
|
defSpec.name = "Default";
|
||
|
|
std::string defPath = getXthemesDir() + "/Default.xth";
|
||
|
|
writeTextFile(defPath, defSpec.serializeXresources());
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!applyArg.empty()) {
|
||
|
|
std::string path = applyArg;
|
||
|
|
if (path.find('/') == std::string::npos) {
|
||
|
|
if (path.size() <= 4 || path.substr(path.size() - 4) != ".xth") {
|
||
|
|
path += ".xth";
|
||
|
|
}
|
||
|
|
path = getXthemesDir() + "/" + path;
|
||
|
|
}
|
||
|
|
ThemeSpec ts;
|
||
|
|
if (!ts.loadFromFile(path)) {
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return applyThemeSpecSystemWide(ts) ? 0 : 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string daemonPidPath = getPluginDaemonPidPath("themes");
|
||
|
|
std::string activePidPath = getRuntimeFile("xthemes.pid");
|
||
|
|
|
||
|
|
if (isPluginDaemonAlive("themes")) {
|
||
|
|
if (!isPreload) {
|
||
|
|
sendPluginCommand("themes",
|
||
|
|
openArg.empty() ? "OPEN" : ("OPEN " + openArg));
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
||
|
|
int fifoFd = setupPluginDaemonFifo("themes");
|
||
|
|
|
||
|
|
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[] = "xthemes";
|
||
|
|
char resClass[] = "XThemes";
|
||
|
|
ch.res_name = resName;
|
||
|
|
ch.res_class = resClass;
|
||
|
|
XSetClassHint(dpy, win, &ch);
|
||
|
|
XStoreName(dpy, win, "Themes");
|
||
|
|
|
||
|
|
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* layoutSmall = pango_layout_new(pangoCtx);
|
||
|
|
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
|
||
|
|
|
||
|
|
PluginTheme pt;
|
||
|
|
pt.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon,
|
||
|
|
nullptr, win);
|
||
|
|
auto& colWhite = pt.colWhite;
|
||
|
|
auto& colLight = pt.colLight;
|
||
|
|
auto& colMuted = pt.colMuted;
|
||
|
|
auto& colYellow = pt.colYellow;
|
||
|
|
auto& colGreen = pt.colGreen;
|
||
|
|
auto& colBlue = pt.colBlue;
|
||
|
|
auto& colRed = pt.colRed;
|
||
|
|
|
||
|
|
ViewMode mode = ViewMode::Browser;
|
||
|
|
std::vector<ThemeFileEntry> themes;
|
||
|
|
int selected = 0;
|
||
|
|
int actionSelected = 0;
|
||
|
|
int confirmSelected = 0;
|
||
|
|
|
||
|
|
// Customize state
|
||
|
|
ThemeSpec editSpec;
|
||
|
|
std::string editThemeName;
|
||
|
|
int editField = 0; // 0..9
|
||
|
|
constexpr int kNumEditFields = 10;
|
||
|
|
|
||
|
|
// Hex color input modal state
|
||
|
|
std::string hexInput;
|
||
|
|
int hexTargetField = 2;
|
||
|
|
int lastTapDigit = -1;
|
||
|
|
int lastTapCycle = 0;
|
||
|
|
int64_t lastTapMs = 0;
|
||
|
|
|
||
|
|
// Name input modal state
|
||
|
|
NameModalAction nameAction = NameModalAction::SaveCurrentAs;
|
||
|
|
std::string nameTitle;
|
||
|
|
std::string nameInput;
|
||
|
|
TextCaseMode caseMode = TextCaseMode::Title;
|
||
|
|
|
||
|
|
std::string statusBanner;
|
||
|
|
int64_t statusBannerUntilMs = 0;
|
||
|
|
|
||
|
|
struct ActionEntry {
|
||
|
|
int id;
|
||
|
|
std::string icon;
|
||
|
|
std::string label;
|
||
|
|
};
|
||
|
|
std::vector<ActionEntry> actionsList;
|
||
|
|
|
||
|
|
bool visible = false;
|
||
|
|
int64_t openStartMs = 0;
|
||
|
|
int64_t startupIgnoreUntilMs = 0;
|
||
|
|
bool initialSoftkeysReleased = true;
|
||
|
|
int softkeyPending = 0;
|
||
|
|
int64_t softkeyDeadlineMs = 0;
|
||
|
|
|
||
|
|
auto refreshThemes = [&](const std::string& selectName = "") {
|
||
|
|
pt.load(dpy, screen, visual, cmap, layoutMain, layoutSmall, layoutIcon,
|
||
|
|
nullptr, win);
|
||
|
|
themes = loadInstalledThemes(pt.spec);
|
||
|
|
if (!selectName.empty()) {
|
||
|
|
for (size_t i = 0; i < themes.size(); ++i) {
|
||
|
|
if (toLowerStr(themes[i].name) == toLowerStr(selectName)) {
|
||
|
|
selected = static_cast<int>(i);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (selected >= static_cast<int>(themes.size())) {
|
||
|
|
selected = std::max(0, static_cast<int>(themes.size()) - 1);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
refreshThemes();
|
||
|
|
|
||
|
|
auto getEditFieldColorPtr = [&](int fieldIdx) -> uint32_t* {
|
||
|
|
switch (fieldIdx) {
|
||
|
|
case 2: return &editSpec.bgMain;
|
||
|
|
case 3: return &editSpec.bgBar;
|
||
|
|
case 4: return &editSpec.bgHeader;
|
||
|
|
case 5: return &editSpec.selBg;
|
||
|
|
case 6: return &editSpec.selFg;
|
||
|
|
case 7: return &editSpec.accent;
|
||
|
|
case 8: return &editSpec.fgMain;
|
||
|
|
case 9: return &editSpec.fgMuted;
|
||
|
|
default: return nullptr;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto syncCustomColorsToDerived = [&](bool keepBarAndHeader = true) {
|
||
|
|
uint32_t savedBar = editSpec.bgBar;
|
||
|
|
uint32_t savedHdr = editSpec.bgHeader;
|
||
|
|
uint32_t savedMuted = editSpec.fgMuted;
|
||
|
|
editSpec.recomputeDerivedFromCore();
|
||
|
|
if (keepBarAndHeader) {
|
||
|
|
editSpec.bgBar = savedBar;
|
||
|
|
editSpec.bgHeader = savedHdr;
|
||
|
|
}
|
||
|
|
editSpec.fgMuted = savedMuted;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto cycleEditField = [&](int fieldIdx, int dir) {
|
||
|
|
if (fieldIdx == 0) {
|
||
|
|
int curIdx = 0;
|
||
|
|
for (int i = 0; i < kNumFontPresets; ++i) {
|
||
|
|
if (toLowerStr(editSpec.fontFace) == toLowerStr(kFontPresets[i])) {
|
||
|
|
curIdx = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
curIdx = (curIdx + dir + kNumFontPresets) % kNumFontPresets;
|
||
|
|
editSpec.fontFace = kFontPresets[curIdx];
|
||
|
|
} else if (fieldIdx == 1) {
|
||
|
|
editSpec.baseFontSize = std::clamp(editSpec.baseFontSize + dir, 11, 22);
|
||
|
|
} else {
|
||
|
|
uint32_t* colPtr = getEditFieldColorPtr(fieldIdx);
|
||
|
|
if (!colPtr) return;
|
||
|
|
int curIdx = -1;
|
||
|
|
for (int i = 0; i < kNumColorPresets; ++i) {
|
||
|
|
uint32_t p = 0;
|
||
|
|
if (parseHexRgb(kColorPresets[i], &p) && p == *colPtr) {
|
||
|
|
curIdx = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (curIdx < 0) {
|
||
|
|
curIdx = (dir > 0) ? 0 : (kNumColorPresets - 1);
|
||
|
|
} else {
|
||
|
|
curIdx = (curIdx + dir + kNumColorPresets) % kNumColorPresets;
|
||
|
|
}
|
||
|
|
uint32_t nextCol = *colPtr;
|
||
|
|
if (parseHexRgb(kColorPresets[curIdx], &nextCol)) {
|
||
|
|
*colPtr = nextCol;
|
||
|
|
syncCustomColorsToDerived(fieldIdx != 2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto buildActionsList = [&]() {
|
||
|
|
actionsList.clear();
|
||
|
|
if (!themes.empty()) {
|
||
|
|
actionsList.push_back({1, kIconCheck, "Apply Selected Theme"});
|
||
|
|
actionsList.push_back({2, kIconPalette, "Customize Theme Colors/Font"});
|
||
|
|
}
|
||
|
|
actionsList.push_back({3, kIconPlus, "Save Current as New Theme"});
|
||
|
|
if (!themes.empty()) {
|
||
|
|
actionsList.push_back({4, kIconCopy, "Duplicate Theme"});
|
||
|
|
actionsList.push_back({5, kIconEdit, "Edit .xth in Nano"});
|
||
|
|
}
|
||
|
|
actionsList.push_back({6, kIconSort, "Reset Default Theme"});
|
||
|
|
if (!themes.empty() && themes[selected].name != "Default") {
|
||
|
|
actionsList.push_back({7, kIconTrash, "Delete Theme"});
|
||
|
|
}
|
||
|
|
actionSelected = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto applySelectedTheme = [&]() {
|
||
|
|
if (themes.empty()) return;
|
||
|
|
ThemeSpec spec = themes[selected].spec;
|
||
|
|
spec.name = themes[selected].name;
|
||
|
|
if (applyThemeSpecSystemWide(spec, dpy, "themes")) {
|
||
|
|
refreshThemes(spec.name);
|
||
|
|
statusBanner = "Applied theme: " + spec.name;
|
||
|
|
statusBannerUntilMs = nowMs() + 2000;
|
||
|
|
} else {
|
||
|
|
statusBanner = "Failed to write ~/.Xresources";
|
||
|
|
statusBannerUntilMs = nowMs() + 2000;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
int tw = 0, th = 0;
|
||
|
|
pango_layout_get_pixel_size(layout, &tw, &th);
|
||
|
|
int dy = y + std::max(0, (h - th) / 2);
|
||
|
|
pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE,
|
||
|
|
dy * PANGO_SCALE);
|
||
|
|
return tw;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto drawSwatch = [&](int x, int y, int w, int h, unsigned long color) {
|
||
|
|
XSetForeground(dpy, gc, color);
|
||
|
|
XFillRectangle(dpy, pix, gc, x, y, w, h);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.border);
|
||
|
|
XDrawRectangle(dpy, pix, gc, x, y, w - 1, h - 1);
|
||
|
|
};
|
||
|
|
|
||
|
|
auto drawSoftkeyBar = [&](const std::string& leftLabel,
|
||
|
|
const std::string& centerLabel,
|
||
|
|
const std::string& rightLabel) {
|
||
|
|
const int barY = kWinHeight - 26;
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgFooter);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 26);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.divider);
|
||
|
|
XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY);
|
||
|
|
|
||
|
|
drawPango(layoutMain, &colBlue, 6, barY, 26, leftLabel);
|
||
|
|
|
||
|
|
pango_layout_set_width(layoutMain, -1);
|
||
|
|
pango_layout_set_text(layoutMain, centerLabel.c_str(),
|
||
|
|
static_cast<int>(centerLabel.size()));
|
||
|
|
int cw = 0, chh = 0;
|
||
|
|
pango_layout_get_pixel_size(layoutMain, &cw, &chh);
|
||
|
|
drawPango(layoutMain, &colWhite, (kWinWidth - cw) / 2, barY, 26,
|
||
|
|
centerLabel);
|
||
|
|
|
||
|
|
pango_layout_set_width(layoutMain, -1);
|
||
|
|
pango_layout_set_text(layoutMain, rightLabel.c_str(),
|
||
|
|
static_cast<int>(rightLabel.size()));
|
||
|
|
int rw = 0, rhh = 0;
|
||
|
|
pango_layout_get_pixel_size(layoutMain, &rw, &rhh);
|
||
|
|
drawPango(layoutMain, &colLight, kWinWidth - rw - 6, barY, 26, rightLabel);
|
||
|
|
};
|
||
|
|
|
||
|
|
auto drawUI = [&]() {
|
||
|
|
if (!visible) return;
|
||
|
|
if (pt.checkAndReload(dpy, screen, visual, cmap, layoutMain, layoutSmall,
|
||
|
|
layoutIcon, nullptr, win)) {
|
||
|
|
themes = loadInstalledThemes(pt.spec);
|
||
|
|
if (selected >= static_cast<int>(themes.size())) {
|
||
|
|
selected = std::max(0, static_cast<int>(themes.size()) - 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgMain);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
||
|
|
|
||
|
|
if (mode == ViewMode::Browser) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.border);
|
||
|
|
XDrawLine(dpy, pix, gc, 0, 35, kWinWidth, 35);
|
||
|
|
|
||
|
|
drawPango(layoutIcon, &colYellow, 6, 1, 34, kIconPalette);
|
||
|
|
drawPango(layoutMain, &colWhite, 26, 1, 34, "Themes (~/.xthemes)", 180);
|
||
|
|
|
||
|
|
std::string cnt = "[" + std::to_string(themes.size()) + "]";
|
||
|
|
pango_layout_set_width(layoutSmall, -1);
|
||
|
|
pango_layout_set_text(layoutSmall, cnt.c_str(),
|
||
|
|
static_cast<int>(cnt.size()));
|
||
|
|
int cw = 0, chh = 0;
|
||
|
|
pango_layout_get_pixel_size(layoutSmall, &cw, &chh);
|
||
|
|
drawPango(layoutSmall, &colMuted, kWinWidth - cw - 6, 1, 34, cnt);
|
||
|
|
|
||
|
|
const int listY = 36;
|
||
|
|
const int rowH = 34;
|
||
|
|
const int visibleRows = 5;
|
||
|
|
|
||
|
|
if (themes.empty()) {
|
||
|
|
drawPango(layoutMain, &colMuted, 16, 100, 24, "No themes found", 208);
|
||
|
|
} else {
|
||
|
|
int scroll = 0;
|
||
|
|
if (selected >= visibleRows) {
|
||
|
|
scroll = selected - visibleRows + 1;
|
||
|
|
}
|
||
|
|
for (int i = 0; i < visibleRows; ++i) {
|
||
|
|
int idx = scroll + i;
|
||
|
|
if (idx >= static_cast<int>(themes.size())) break;
|
||
|
|
const auto& th = themes[idx];
|
||
|
|
int ry = listY + i * rowH;
|
||
|
|
bool isSel = (idx == selected);
|
||
|
|
|
||
|
|
if (isSel) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.selBg);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.accentAlt);
|
||
|
|
XDrawLine(dpy, pix, gc, 0, ry, kWinWidth, ry);
|
||
|
|
} else {
|
||
|
|
XSetForeground(dpy, gc,
|
||
|
|
(i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
drawPango(layoutIcon,
|
||
|
|
th.isActive ? &colGreen : (isSel ? &colYellow : &colBlue),
|
||
|
|
6, ry, rowH, th.isActive ? kIconCheck : kIconPalette);
|
||
|
|
|
||
|
|
std::string label = th.name + (th.isActive ? " *" : "");
|
||
|
|
drawPango(layoutMain, &colWhite, 28, ry, rowH, label, 130);
|
||
|
|
|
||
|
|
// Draw 4 color swatches on the right side of the row
|
||
|
|
int swX = kWinWidth - 72;
|
||
|
|
int swY = ry + (rowH - 14) / 2;
|
||
|
|
drawSwatch(swX, swY, 14, 14, th.spec.bgMain);
|
||
|
|
drawSwatch(swX + 16, swY, 14, 14, th.spec.selBg);
|
||
|
|
drawSwatch(swX + 32, swY, 14, 14, th.spec.accent);
|
||
|
|
drawSwatch(swX + 48, swY, 14, 14, th.spec.fgMain);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bottom live preview card of the selected theme (y = 208..268)
|
||
|
|
const auto& selTh = themes[selected];
|
||
|
|
const int cardY = 208;
|
||
|
|
const int cardH = 58;
|
||
|
|
XSetForeground(dpy, gc, selTh.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 8, cardH);
|
||
|
|
XSetForeground(dpy, gc, selTh.spec.selBg);
|
||
|
|
XDrawRectangle(dpy, pix, gc, 4, cardY, kWinWidth - 9, cardH - 1);
|
||
|
|
|
||
|
|
std::string line1 = selTh.name + ".xth | " + selTh.spec.fontFace +
|
||
|
|
" " + std::to_string(selTh.spec.baseFontSize) + "px";
|
||
|
|
drawPango(layoutSmall, &colYellow, 10, cardY + 3, 18, line1,
|
||
|
|
kWinWidth - 20);
|
||
|
|
|
||
|
|
std::string line2 =
|
||
|
|
"BG:" + formatHexRgb(selTh.spec.bgMain) +
|
||
|
|
" SEL:" + formatHexRgb(selTh.spec.selBg) +
|
||
|
|
" ACC:" + formatHexRgb(selTh.spec.accent);
|
||
|
|
drawPango(layoutSmall, &colLight, 10, cardY + 21, 18, line2,
|
||
|
|
kWinWidth - 20);
|
||
|
|
|
||
|
|
std::string line3 =
|
||
|
|
"FG:" + formatHexRgb(selTh.spec.fgMain) +
|
||
|
|
" Font:" + std::to_string(selTh.spec.sizeSmall()) + "/" +
|
||
|
|
std::to_string(selTh.spec.sizeMain()) + "/" +
|
||
|
|
std::to_string(selTh.spec.sizeIcon()) + "/" +
|
||
|
|
std::to_string(selTh.spec.sizeLarge()) + "px";
|
||
|
|
drawPango(layoutSmall, &colMuted, 10, cardY + 38, 18, line3,
|
||
|
|
kWinWidth - 20);
|
||
|
|
}
|
||
|
|
|
||
|
|
drawSoftkeyBar("Options", "Apply", "Close");
|
||
|
|
|
||
|
|
} else if (mode == ViewMode::Actions) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 34);
|
||
|
|
drawPango(layoutIcon, &colBlue, 6, 0, 34, kIconList);
|
||
|
|
drawPango(layoutMain, &colWhite, 28, 0, 34, "Theme Options", 200);
|
||
|
|
|
||
|
|
const int listY = 36;
|
||
|
|
const int rowH = 33;
|
||
|
|
const int visibleRows = 7;
|
||
|
|
int scroll = 0;
|
||
|
|
if (actionSelected >= visibleRows) {
|
||
|
|
scroll = actionSelected - visibleRows + 1;
|
||
|
|
}
|
||
|
|
for (int i = 0; i < visibleRows; ++i) {
|
||
|
|
int idx = scroll + i;
|
||
|
|
if (idx >= static_cast<int>(actionsList.size())) break;
|
||
|
|
int ry = listY + i * rowH;
|
||
|
|
bool isSel = (idx == actionSelected);
|
||
|
|
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgMain);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1);
|
||
|
|
drawPango(layoutIcon, isSel ? &colYellow : &colBlue, 8, ry, rowH,
|
||
|
|
actionsList[idx].icon);
|
||
|
|
drawPango(layoutMain, &colWhite, 32, ry, rowH, actionsList[idx].label,
|
||
|
|
202);
|
||
|
|
}
|
||
|
|
drawSoftkeyBar("Select", "Select", "Back");
|
||
|
|
|
||
|
|
} else if (mode == ViewMode::Customize) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 32);
|
||
|
|
drawPango(layoutIcon, &colYellow, 6, 0, 32, kIconPalette);
|
||
|
|
drawPango(layoutMain, &colWhite, 28, 0, 32,
|
||
|
|
"Edit: " + editThemeName, 204);
|
||
|
|
|
||
|
|
struct FieldRow {
|
||
|
|
const char* label;
|
||
|
|
std::string value;
|
||
|
|
bool hasSwatch;
|
||
|
|
unsigned long swatchColor;
|
||
|
|
};
|
||
|
|
FieldRow rows[kNumEditFields] = {
|
||
|
|
{"Font Face", editSpec.fontFace, false, 0},
|
||
|
|
{"Base Size",
|
||
|
|
std::to_string(editSpec.baseFontSize) + "px (" +
|
||
|
|
std::to_string(editSpec.sizeSmall()) + "/" +
|
||
|
|
std::to_string(editSpec.sizeMain()) + "/" +
|
||
|
|
std::to_string(editSpec.sizeLarge()) + ")",
|
||
|
|
false, 0},
|
||
|
|
{"Main BG", formatHexRgb(editSpec.bgMain), true, editSpec.bgMain},
|
||
|
|
{"Bar BG", formatHexRgb(editSpec.bgBar), true, editSpec.bgBar},
|
||
|
|
{"Header BG", formatHexRgb(editSpec.bgHeader), true, editSpec.bgHeader},
|
||
|
|
{"Selected BG", formatHexRgb(editSpec.selBg), true, editSpec.selBg},
|
||
|
|
{"Selected FG", formatHexRgb(editSpec.selFg), true, editSpec.selFg},
|
||
|
|
{"Accent", formatHexRgb(editSpec.accent), true, editSpec.accent},
|
||
|
|
{"Font Color", formatHexRgb(editSpec.fgMain), true, editSpec.fgMain},
|
||
|
|
{"Muted Color", formatHexRgb(editSpec.fgMuted), true, editSpec.fgMuted}
|
||
|
|
};
|
||
|
|
|
||
|
|
const int listY = 34;
|
||
|
|
const int rowH = 29;
|
||
|
|
const int visibleRows = 8;
|
||
|
|
int scroll = 0;
|
||
|
|
if (editField >= visibleRows) {
|
||
|
|
scroll = editField - visibleRows + 1;
|
||
|
|
}
|
||
|
|
for (int i = 0; i < visibleRows; ++i) {
|
||
|
|
int idx = scroll + i;
|
||
|
|
if (idx >= kNumEditFields) break;
|
||
|
|
int ry = listY + i * rowH;
|
||
|
|
bool isSel = (idx == editField);
|
||
|
|
XSetForeground(
|
||
|
|
dpy, gc,
|
||
|
|
isSel ? pt.spec.selBg
|
||
|
|
: ((i % 2 == 0) ? pt.spec.bgAltEven : pt.spec.bgAltOdd));
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH - 1);
|
||
|
|
|
||
|
|
drawPango(layoutSmall, isSel ? &colYellow : &colLight, 6, ry, rowH,
|
||
|
|
rows[idx].label, 88);
|
||
|
|
|
||
|
|
if (rows[idx].hasSwatch) {
|
||
|
|
drawSwatch(98, ry + (rowH - 14) / 2, 16, 14, rows[idx].swatchColor);
|
||
|
|
drawPango(layoutMain, &colWhite, 120, ry, rowH,
|
||
|
|
"< " + rows[idx].value + " >", 114);
|
||
|
|
} else {
|
||
|
|
drawPango(layoutSmall, &colWhite, 96, ry, rowH,
|
||
|
|
"< " + rows[idx].value + " >", 138);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
drawSoftkeyBar("Save", editField >= 2 ? "Hex#" : "Next", "Back");
|
||
|
|
|
||
|
|
} else if (mode == ViewMode::HexModal) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36);
|
||
|
|
drawPango(layoutIcon, &colYellow, 6, 0, 36, kIconPalette);
|
||
|
|
drawPango(layoutMain, &colWhite, 28, 0, 36, "Enter Hex Color", 200);
|
||
|
|
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgInputFocus);
|
||
|
|
XFillRectangle(dpy, pix, gc, 12, 66, kWinWidth - 24, 38);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.accentAlt);
|
||
|
|
XDrawRectangle(dpy, pix, gc, 12, 66, kWinWidth - 25, 37);
|
||
|
|
|
||
|
|
std::string disp = "#" + hexInput + "|";
|
||
|
|
drawPango(layoutMain, &colWhite, 20, 66, 38, disp, 140);
|
||
|
|
|
||
|
|
uint32_t previewCol = 0;
|
||
|
|
std::string candidate = "#" + hexInput;
|
||
|
|
if (parseHexRgb(candidate, &previewCol)) {
|
||
|
|
drawSwatch(kWinWidth - 60, 71, 40, 28, previewCol);
|
||
|
|
}
|
||
|
|
|
||
|
|
drawPango(layoutSmall, &colLight, 12, 118, 20,
|
||
|
|
"Keys 0-9: digits / hex A-F", 216);
|
||
|
|
drawPango(layoutSmall, &colMuted, 12, 140, 20,
|
||
|
|
"2: 2/A/B/C 3: 3/D/E/F", 216);
|
||
|
|
drawSoftkeyBar("OK", "OK", hexInput.empty() ? "Cancel" : "Del");
|
||
|
|
|
||
|
|
} else if (mode == ViewMode::NameModal) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36);
|
||
|
|
drawPango(layoutIcon, &colBlue, 6, 0, 36, kIconEdit);
|
||
|
|
drawPango(layoutMain, &colWhite, 28, 0, 36, nameTitle, 145);
|
||
|
|
|
||
|
|
const char* mBadge = "[Abc]";
|
||
|
|
if (caseMode == TextCaseMode::Lower) mBadge = "[abc]";
|
||
|
|
else if (caseMode == TextCaseMode::Upper) mBadge = "[ABC]";
|
||
|
|
else if (caseMode == TextCaseMode::Num) mBadge = "[123]";
|
||
|
|
drawPango(layoutSmall, &colYellow, kWinWidth - 46, 0, 36, mBadge);
|
||
|
|
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgInputFocus);
|
||
|
|
XFillRectangle(dpy, pix, gc, 10, 74, kWinWidth - 20, 38);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.accentAlt);
|
||
|
|
XDrawRectangle(dpy, pix, gc, 10, 74, kWinWidth - 21, 37);
|
||
|
|
drawPango(layoutMain, &colWhite, 16, 74, 38, nameInput + "|",
|
||
|
|
kWinWidth - 32);
|
||
|
|
|
||
|
|
drawPango(layoutSmall, &colMuted, 10, 124, 20,
|
||
|
|
"Saves to ~/.xthemes/<Name>.xth", 220);
|
||
|
|
drawPango(layoutSmall, &colMuted, 10, 146, 20,
|
||
|
|
"0-9: multi-tap #: cycle case", 220);
|
||
|
|
drawSoftkeyBar("Save", "Save", nameInput.empty() ? "Cancel" : "Del");
|
||
|
|
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete && !themes.empty()) {
|
||
|
|
const auto& th = themes[selected];
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 36);
|
||
|
|
drawPango(layoutIcon, &colRed, 8, 0, 36, kIconTrash);
|
||
|
|
drawPango(layoutMain, &colWhite, 30, 0, 36, "Delete Theme?", 200);
|
||
|
|
|
||
|
|
drawPango(layoutMain, &colWhite, 14, 68, 26, th.name + ".xth", 212);
|
||
|
|
drawPango(layoutSmall, &colMuted, 14, 98, 22,
|
||
|
|
"Remove this theme file from", 212);
|
||
|
|
drawPango(layoutSmall, &colMuted, 14, 118, 22, "~/.xthemes/?", 212);
|
||
|
|
|
||
|
|
for (int i = 0; i < 2; ++i) {
|
||
|
|
int ry = 164 + i * 40;
|
||
|
|
bool isSel = (confirmSelected == i);
|
||
|
|
XSetForeground(
|
||
|
|
dpy, gc,
|
||
|
|
isSel ? (i == 1 ? pt.spec.dangerBg : pt.spec.selBg)
|
||
|
|
: pt.spec.bgButton);
|
||
|
|
XFillRectangle(dpy, pix, gc, 16, ry, kWinWidth - 32, 34);
|
||
|
|
drawPango(layoutMain, &colWhite, 28, ry, 34,
|
||
|
|
i == 0 ? "No, Keep Theme" : "Yes, Delete Theme", 184);
|
||
|
|
}
|
||
|
|
drawSoftkeyBar("Select", "Confirm", "Cancel");
|
||
|
|
}
|
||
|
|
|
||
|
|
int64_t tNow = nowMs();
|
||
|
|
if (!statusBanner.empty() && tNow < statusBannerUntilMs) {
|
||
|
|
XSetForeground(dpy, gc, pt.spec.bgToast);
|
||
|
|
XFillRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 16, 28);
|
||
|
|
XSetForeground(dpy, gc, pt.spec.accent);
|
||
|
|
XDrawRectangle(dpy, pix, gc, 8, kWinHeight - 58, kWinWidth - 17, 27);
|
||
|
|
drawPango(layoutSmall, &colWhite, 14, kWinHeight - 58, 28, statusBanner,
|
||
|
|
kWinWidth - 28);
|
||
|
|
}
|
||
|
|
|
||
|
|
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
||
|
|
XFlush(dpy);
|
||
|
|
};
|
||
|
|
|
||
|
|
auto showWindow = [&](const std::string& argStr) {
|
||
|
|
closeOtherPlugins("themes");
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
refreshThemes(argStr);
|
||
|
|
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
||
|
|
if (!visible) {
|
||
|
|
visible = true;
|
||
|
|
XMapRaised(dpy, win);
|
||
|
|
for (int i = 0; i < 20; ++i) {
|
||
|
|
if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync,
|
||
|
|
CurrentTime) == GrabSuccess) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
usleep(2000);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
XRaiseWindow(dpy, win);
|
||
|
|
}
|
||
|
|
XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime);
|
||
|
|
openStartMs = nowMs();
|
||
|
|
startupIgnoreUntilMs = openStartMs + 180;
|
||
|
|
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
||
|
|
softkeyPending = 0;
|
||
|
|
drawUI();
|
||
|
|
};
|
||
|
|
|
||
|
|
auto hideWindow = [&]() {
|
||
|
|
if (!visible) return;
|
||
|
|
writeSoftkeySuppress(nowMs() + 450);
|
||
|
|
XUngrabKeyboard(dpy, CurrentTime);
|
||
|
|
XUnmapWindow(dpy, win);
|
||
|
|
XFlush(dpy);
|
||
|
|
visible = false;
|
||
|
|
::unlink(activePidPath.c_str());
|
||
|
|
};
|
||
|
|
|
||
|
|
auto commitNameTap = [&](int digit) {
|
||
|
|
const char* chars = kMultiTapTheme[digit];
|
||
|
|
int len = static_cast<int>(std::strlen(chars));
|
||
|
|
if (len <= 0) return;
|
||
|
|
|
||
|
|
int64_t t = nowMs();
|
||
|
|
if (caseMode == TextCaseMode::Num) {
|
||
|
|
nameInput.push_back(static_cast<char>('0' + digit));
|
||
|
|
lastTapDigit = -1;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool sameKey = (lastTapDigit == digit && (t - lastTapMs) <= 850 &&
|
||
|
|
!nameInput.empty());
|
||
|
|
if (sameKey) {
|
||
|
|
lastTapCycle = (lastTapCycle + 1) % len;
|
||
|
|
popUtf8(nameInput);
|
||
|
|
} else {
|
||
|
|
lastTapDigit = digit;
|
||
|
|
lastTapCycle = 0;
|
||
|
|
}
|
||
|
|
lastTapMs = t;
|
||
|
|
|
||
|
|
char ch = chars[lastTapCycle];
|
||
|
|
if (std::isalpha(static_cast<unsigned char>(ch))) {
|
||
|
|
if (caseMode == TextCaseMode::Upper) {
|
||
|
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||
|
|
} else if (caseMode == TextCaseMode::Title && nameInput.empty()) {
|
||
|
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
nameInput.push_back(ch);
|
||
|
|
};
|
||
|
|
|
||
|
|
auto commitHexTap = [&](int digit) {
|
||
|
|
const char* chars = kHexKeyMap[digit];
|
||
|
|
int len = static_cast<int>(std::strlen(chars));
|
||
|
|
if (len <= 0) return;
|
||
|
|
|
||
|
|
int64_t t = nowMs();
|
||
|
|
bool sameKey = (lastTapDigit == digit && (t - lastTapMs) <= 800 &&
|
||
|
|
len > 1 && !hexInput.empty());
|
||
|
|
if (sameKey) {
|
||
|
|
lastTapCycle = (lastTapCycle + 1) % len;
|
||
|
|
hexInput.pop_back();
|
||
|
|
} else {
|
||
|
|
if (hexInput.size() >= 6) return;
|
||
|
|
lastTapDigit = digit;
|
||
|
|
lastTapCycle = 0;
|
||
|
|
}
|
||
|
|
lastTapMs = t;
|
||
|
|
hexInput.push_back(chars[lastTapCycle]);
|
||
|
|
};
|
||
|
|
|
||
|
|
auto executeAction = [&](int actId) {
|
||
|
|
if (actId == 1) {
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
applySelectedTheme();
|
||
|
|
} else if (actId == 2 && !themes.empty()) {
|
||
|
|
editSpec = themes[selected].spec;
|
||
|
|
editThemeName = themes[selected].name;
|
||
|
|
editField = 0;
|
||
|
|
mode = ViewMode::Customize;
|
||
|
|
} else if (actId == 3) {
|
||
|
|
nameAction = NameModalAction::SaveCurrentAs;
|
||
|
|
nameTitle = "Save Theme As";
|
||
|
|
nameInput = "Custom";
|
||
|
|
lastTapDigit = -1;
|
||
|
|
mode = ViewMode::NameModal;
|
||
|
|
} else if (actId == 4 && !themes.empty()) {
|
||
|
|
nameAction = NameModalAction::DuplicateTheme;
|
||
|
|
nameTitle = "Duplicate Theme";
|
||
|
|
nameInput = themes[selected].name + "_Copy";
|
||
|
|
lastTapDigit = -1;
|
||
|
|
mode = ViewMode::NameModal;
|
||
|
|
} else if (actId == 5 && !themes.empty()) {
|
||
|
|
std::string p = themes[selected].fullPath;
|
||
|
|
hideWindow();
|
||
|
|
runAsyncCmd("st -e nano \"" + p + "\"");
|
||
|
|
} else if (actId == 6) {
|
||
|
|
ThemeSpec defSpec;
|
||
|
|
defSpec.name = "Default";
|
||
|
|
writeTextFile(getXthemesDir() + "/Default.xth",
|
||
|
|
defSpec.serializeXresources());
|
||
|
|
applyThemeSpecSystemWide(defSpec, dpy);
|
||
|
|
refreshThemes("Default");
|
||
|
|
statusBanner = "Reset & applied Default theme";
|
||
|
|
statusBannerUntilMs = nowMs() + 2000;
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
} else if (actId == 7 && !themes.empty()) {
|
||
|
|
confirmSelected = 0;
|
||
|
|
mode = ViewMode::ConfirmDelete;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto handleSoftkeyAction = [&](bool isLeft) {
|
||
|
|
if (mode == ViewMode::Browser) {
|
||
|
|
if (isLeft) {
|
||
|
|
buildActionsList();
|
||
|
|
mode = ViewMode::Actions;
|
||
|
|
} else {
|
||
|
|
hideWindow();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Actions) {
|
||
|
|
if (isLeft) {
|
||
|
|
if (!actionsList.empty()) {
|
||
|
|
executeAction(actionsList[actionSelected].id);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Customize) {
|
||
|
|
if (isLeft) {
|
||
|
|
editSpec.name = editThemeName;
|
||
|
|
std::string targetPath = getXthemesDir() + "/" + editThemeName + ".xth";
|
||
|
|
writeTextFile(targetPath, editSpec.serializeXresources());
|
||
|
|
applyThemeSpecSystemWide(editSpec, dpy);
|
||
|
|
refreshThemes(editThemeName);
|
||
|
|
statusBanner = "Saved & applied " + editThemeName;
|
||
|
|
statusBannerUntilMs = nowMs() + 2000;
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::HexModal) {
|
||
|
|
if (isLeft) {
|
||
|
|
uint32_t parsed = 0;
|
||
|
|
if (parseHexRgb("#" + hexInput, &parsed)) {
|
||
|
|
uint32_t* colPtr = getEditFieldColorPtr(hexTargetField);
|
||
|
|
if (colPtr) {
|
||
|
|
*colPtr = parsed;
|
||
|
|
syncCustomColorsToDerived(hexTargetField != 2);
|
||
|
|
}
|
||
|
|
mode = ViewMode::Customize;
|
||
|
|
} else {
|
||
|
|
statusBanner = "Enter 6 hex digits (0-9, a-f)";
|
||
|
|
statusBannerUntilMs = nowMs() + 1800;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if (!hexInput.empty()) {
|
||
|
|
hexInput.pop_back();
|
||
|
|
lastTapDigit = -1;
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::Customize;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::NameModal) {
|
||
|
|
if (isLeft) {
|
||
|
|
std::string cleanName = trimStr(nameInput);
|
||
|
|
// Strip any slash or .xth suffix
|
||
|
|
if (cleanName.size() > 4 &&
|
||
|
|
cleanName.substr(cleanName.size() - 4) == ".xth") {
|
||
|
|
cleanName = cleanName.substr(0, cleanName.size() - 4);
|
||
|
|
}
|
||
|
|
for (char& c : cleanName) {
|
||
|
|
if (c == '/' || c == '\\' || c == ':') c = '_';
|
||
|
|
}
|
||
|
|
if (!cleanName.empty()) {
|
||
|
|
ThemeSpec toSave =
|
||
|
|
(nameAction == NameModalAction::DuplicateTheme && !themes.empty())
|
||
|
|
? themes[selected].spec
|
||
|
|
: pt.spec;
|
||
|
|
toSave.name = cleanName;
|
||
|
|
std::string outPath = getXthemesDir() + "/" + cleanName + ".xth";
|
||
|
|
if (writeTextFile(outPath, toSave.serializeXresources())) {
|
||
|
|
refreshThemes(cleanName);
|
||
|
|
statusBanner = "Saved " + cleanName + ".xth";
|
||
|
|
statusBannerUntilMs = nowMs() + 2000;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
} else {
|
||
|
|
if (!nameInput.empty()) {
|
||
|
|
popUtf8(nameInput);
|
||
|
|
lastTapDigit = -1;
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete) {
|
||
|
|
if (isLeft) {
|
||
|
|
if (confirmSelected == 1 && !themes.empty() &&
|
||
|
|
themes[selected].name != "Default") {
|
||
|
|
std::string delName = themes[selected].name;
|
||
|
|
::unlink(themes[selected].fullPath.c_str());
|
||
|
|
refreshThemes();
|
||
|
|
statusBanner = "Deleted " + delName + ".xth";
|
||
|
|
statusBannerUntilMs = nowMs() + 1800;
|
||
|
|
}
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::Browser;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
drawUI();
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!isPreload) {
|
||
|
|
showWindow(openArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
int xfd = ConnectionNumber(dpy);
|
||
|
|
while (gRunning) {
|
||
|
|
for (const auto& cmd : readFifoLines(fifoFd)) {
|
||
|
|
if (cmd == "NOP") {
|
||
|
|
continue;
|
||
|
|
} else if (cmd == "RELOAD_THEME") {
|
||
|
|
refreshThemes();
|
||
|
|
if (visible) drawUI();
|
||
|
|
} else if (cmd == "CLOSE") {
|
||
|
|
hideWindow();
|
||
|
|
} else if (cmd == "OPEN") {
|
||
|
|
showWindow("");
|
||
|
|
} else if (cmd.rfind("OPEN ", 0) == 0) {
|
||
|
|
showWindow(trimStr(cmd.substr(5)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
while (XPending(dpy) > 0) {
|
||
|
|
XEvent ev;
|
||
|
|
XNextEvent(dpy, &ev);
|
||
|
|
if (!visible) continue;
|
||
|
|
|
||
|
|
if (ev.type == Expose && ev.xexpose.count == 0) {
|
||
|
|
drawUI();
|
||
|
|
} else if (ev.type == KeyPress) {
|
||
|
|
int64_t tNow = nowMs();
|
||
|
|
unsigned int kc = ev.xkey.keycode;
|
||
|
|
KeySym sym = XLookupKeysym(&ev.xkey, 0);
|
||
|
|
|
||
|
|
bool isLeftSk =
|
||
|
|
(kc == 135 || sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 ||
|
||
|
|
sym == _EVDEVK(0x246) || sym == XK_F1);
|
||
|
|
bool isRightSk =
|
||
|
|
(kc == 166 || sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 ||
|
||
|
|
sym == _EVDEVK(0x247) || sym == XK_F2);
|
||
|
|
|
||
|
|
if (!initialSoftkeysReleased) {
|
||
|
|
if (querySoftkeysDown(dpy) == 0) {
|
||
|
|
initialSoftkeysReleased = true;
|
||
|
|
} else if (isLeftSk || isRightSk) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (tNow < startupIgnoreUntilMs && (isLeftSk || isRightSk)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isLeftSk || isRightSk) {
|
||
|
|
int bit = isLeftSk ? 1 : 2;
|
||
|
|
softkeyPending |= bit;
|
||
|
|
if (softkeyPending == 3 || querySoftkeysDown(dpy) == 3) {
|
||
|
|
softkeyPending = 0;
|
||
|
|
sendPluginCommand("xdrawer", "TOGGLE");
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
softkeyDeadlineMs = tNow + 35;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sym == XK_Escape || kc == 124 || sym == XF86XK_PowerOff) {
|
||
|
|
hideWindow();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sym == XK_BackSpace || sym == XK_Delete) {
|
||
|
|
handleSoftkeyAction(false);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool isCallKey =
|
||
|
|
(kc == 177 || sym == XF86XK_PickupPhone || sym == XF86XK_Phone ||
|
||
|
|
sym == _EVDEVK(0x1bd) || sym == _EVDEVK(0x0a9));
|
||
|
|
|
||
|
|
if (mode == ViewMode::Browser) {
|
||
|
|
if (sym == XK_Up || sym == XK_KP_Up) {
|
||
|
|
if (!themes.empty()) {
|
||
|
|
selected =
|
||
|
|
(selected - 1 + static_cast<int>(themes.size())) %
|
||
|
|
static_cast<int>(themes.size());
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||
|
|
if (!themes.empty()) {
|
||
|
|
selected = (selected + 1) % static_cast<int>(themes.size());
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
} else if (sym == XK_Return || sym == XK_KP_Enter || isCallKey) {
|
||
|
|
applySelectedTheme();
|
||
|
|
drawUI();
|
||
|
|
} else {
|
||
|
|
char d = mapDialKey(sym, kc, ev.xkey.state);
|
||
|
|
if (d == '*' && !themes.empty()) {
|
||
|
|
editSpec = themes[selected].spec;
|
||
|
|
editThemeName = themes[selected].name;
|
||
|
|
editField = 0;
|
||
|
|
mode = ViewMode::Customize;
|
||
|
|
drawUI();
|
||
|
|
} else if (d == '#') {
|
||
|
|
nameAction = NameModalAction::SaveCurrentAs;
|
||
|
|
nameTitle = "Save Theme As";
|
||
|
|
nameInput = "Custom";
|
||
|
|
lastTapDigit = -1;
|
||
|
|
mode = ViewMode::NameModal;
|
||
|
|
drawUI();
|
||
|
|
} else if (d >= '1' && d <= '9') {
|
||
|
|
int idx = (d - '1');
|
||
|
|
if (idx < static_cast<int>(themes.size())) {
|
||
|
|
selected = idx;
|
||
|
|
applySelectedTheme();
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Actions) {
|
||
|
|
if (sym == XK_Up || sym == XK_KP_Up) {
|
||
|
|
if (!actionsList.empty()) {
|
||
|
|
actionSelected =
|
||
|
|
(actionSelected - 1 + static_cast<int>(actionsList.size())) %
|
||
|
|
static_cast<int>(actionsList.size());
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||
|
|
if (!actionsList.empty()) {
|
||
|
|
actionSelected =
|
||
|
|
(actionSelected + 1) % static_cast<int>(actionsList.size());
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||
|
|
handleSoftkeyAction(true);
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Customize) {
|
||
|
|
if (sym == XK_Up || sym == XK_KP_Up) {
|
||
|
|
editField = (editField - 1 + kNumEditFields) % kNumEditFields;
|
||
|
|
drawUI();
|
||
|
|
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||
|
|
editField = (editField + 1) % kNumEditFields;
|
||
|
|
drawUI();
|
||
|
|
} else if (sym == XK_Left || sym == XK_KP_Left) {
|
||
|
|
cycleEditField(editField, -1);
|
||
|
|
drawUI();
|
||
|
|
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
||
|
|
cycleEditField(editField, 1);
|
||
|
|
drawUI();
|
||
|
|
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||
|
|
if (editField >= 2) {
|
||
|
|
uint32_t* colPtr = getEditFieldColorPtr(editField);
|
||
|
|
if (colPtr) {
|
||
|
|
hexTargetField = editField;
|
||
|
|
hexInput = formatHexRgb(*colPtr).substr(1);
|
||
|
|
lastTapDigit = -1;
|
||
|
|
mode = ViewMode::HexModal;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
cycleEditField(editField, 1);
|
||
|
|
}
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::HexModal) {
|
||
|
|
if (sym == XK_Return || sym == XK_KP_Enter) {
|
||
|
|
handleSoftkeyAction(true);
|
||
|
|
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
||
|
|
lastTapDigit = -1;
|
||
|
|
} else {
|
||
|
|
char d = mapDialKey(sym, kc, ev.xkey.state);
|
||
|
|
if (d >= '0' && d <= '9') {
|
||
|
|
commitHexTap(d - '0');
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::NameModal) {
|
||
|
|
if (sym == XK_Return || sym == XK_KP_Enter) {
|
||
|
|
handleSoftkeyAction(true);
|
||
|
|
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
||
|
|
lastTapDigit = -1;
|
||
|
|
} else {
|
||
|
|
char d = mapDialKey(sym, kc, ev.xkey.state);
|
||
|
|
if (d == '#') {
|
||
|
|
lastTapDigit = -1;
|
||
|
|
caseMode = static_cast<TextCaseMode>(
|
||
|
|
(static_cast<int>(caseMode) + 1) % 4);
|
||
|
|
drawUI();
|
||
|
|
} else if (d == '*') {
|
||
|
|
commitNameTap(1);
|
||
|
|
drawUI();
|
||
|
|
} else if (d >= '0' && d <= '9') {
|
||
|
|
commitNameTap(d - '0');
|
||
|
|
drawUI();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete) {
|
||
|
|
if (sym == XK_Up || sym == XK_Down || sym == XK_KP_Up ||
|
||
|
|
sym == XK_KP_Down) {
|
||
|
|
confirmSelected = 1 - confirmSelected;
|
||
|
|
drawUI();
|
||
|
|
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||
|
|
handleSoftkeyAction(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int64_t tNow = nowMs();
|
||
|
|
if (visible && softkeyPending != 0 && tNow >= softkeyDeadlineMs) {
|
||
|
|
if (querySoftkeysDown(dpy) == 3) {
|
||
|
|
softkeyPending = 0;
|
||
|
|
sendPluginCommand("xdrawer", "TOGGLE");
|
||
|
|
} else {
|
||
|
|
bool isLeft = (softkeyPending == 1);
|
||
|
|
softkeyPending = 0;
|
||
|
|
handleSoftkeyAction(isLeft);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int timeoutMs = -1;
|
||
|
|
if (visible && softkeyPending != 0) {
|
||
|
|
timeoutMs = std::max(1, static_cast<int>(softkeyDeadlineMs - tNow));
|
||
|
|
}
|
||
|
|
|
||
|
|
struct pollfd pfds[2];
|
||
|
|
int nfds = 0;
|
||
|
|
pfds[nfds++] = {xfd, POLLIN, 0};
|
||
|
|
int fifoIdx = -1;
|
||
|
|
if (fifoFd >= 0) {
|
||
|
|
fifoIdx = nfds;
|
||
|
|
pfds[nfds++] = {fifoFd, POLLIN, 0};
|
||
|
|
}
|
||
|
|
|
||
|
|
int pr = ::poll(pfds, nfds, timeoutMs);
|
||
|
|
if (pr > 0 && fifoIdx >= 0 && (pfds[fifoIdx].revents & POLLIN)) {
|
||
|
|
for (const auto& cmd : readFifoLines(fifoFd)) {
|
||
|
|
if (cmd == "RELOAD_THEME") {
|
||
|
|
refreshThemes();
|
||
|
|
if (visible) drawUI();
|
||
|
|
} else if (cmd == "CLOSE") {
|
||
|
|
hideWindow();
|
||
|
|
} else if (cmd == "OPEN") {
|
||
|
|
showWindow("");
|
||
|
|
} else if (cmd.rfind("OPEN ", 0) == 0) {
|
||
|
|
showWindow(trimStr(cmd.substr(5)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
hideWindow();
|
||
|
|
if (fifoFd >= 0) ::close(fifoFd);
|
||
|
|
::unlink(daemonPidPath.c_str());
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int themes_plugin_init(const DwmHostApi* host) {
|
||
|
|
if (host && host->spawn_plugin) {
|
||
|
|
host->spawn_plugin("themes", "--preload");
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
||
|
|
DWM_PLUGIN_API_VERSION,
|
||
|
|
"themes",
|
||
|
|
"Xresources theme manager for ~/.xthemes (*.xth) with live system-wide reload",
|
||
|
|
themes_plugin_init,
|
||
|
|
nullptr,
|
||
|
|
nullptr,
|
||
|
|
nullptr,
|
||
|
|
nullptr,
|
||
|
|
runThemesApp
|
||
|
|
};
|