#include "phone_data.h" #include "../sxmo-dwm/dwm_plugin.h" #include #include #include #include #include #include #include #include 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(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(sizeof(kColorPresets) / sizeof(kColorPresets[0])); struct ThemeFileEntry { std::string name; // basename without .xth std::string fullPath; // ~/.xthemes/.xth ThemeSpec spec; bool isActive = false; }; void popUtf8(std::string& s) { if (s.empty()) return; while (!s.empty() && (static_cast(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(xresContent.data()), static_cast(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 loadInstalledThemes(const ThemeSpec& activeSpec) { ensureDefaultThemesSeeded(); std::vector 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(::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 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 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(i); break; } } } if (selected >= static_cast(themes.size())) { selected = std::max(0, static_cast(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(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(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(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(themes.size())) { selected = std::max(0, static_cast(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(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(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(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/.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) { writeIntFile(activePidPath, static_cast(::getpid())); closeOtherPlugins("themes"); mode = ViewMode::Browser; refreshThemes(argStr); if (!visible) { visible = true; XMapRaised(dpy, win); } else { XRaiseWindow(dpy, win); } grabPluginKeyboard(dpy, win); 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(std::strlen(chars)); if (len <= 0) return; int64_t t = nowMs(); if (caseMode == TextCaseMode::Num) { nameInput.push_back(static_cast('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(ch))) { if (caseMode == TextCaseMode::Upper) { ch = static_cast(std::toupper(static_cast(ch))); } else if (caseMode == TextCaseMode::Title && nameInput.empty()) { ch = static_cast(std::toupper(static_cast(ch))); } } nameInput.push_back(ch); }; auto commitHexTap = [&](int digit) { const char* chars = kHexKeyMap[digit]; int len = static_cast(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 == VisibilityNotify && ev.xvisibility.state != VisibilityUnobscured) { if (isAnotherPluginActive("themes")) { hideWindow(); } else { XRaiseWindow(dpy, win); grabPluginKeyboard(dpy, win); 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(themes.size())) % static_cast(themes.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!themes.empty()) { selected = (selected + 1) % static_cast(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(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(actionsList.size())) % static_cast(actionsList.size()); drawUI(); } } else if (sym == XK_Down || sym == XK_KP_Down) { if (!actionsList.empty()) { actionSelected = (actionSelected + 1) % static_cast(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( (static_cast(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(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 };