#ifndef DWM_PLUGINS_THEME_H #define DWM_PLUGINS_THEME_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace bananarchy { inline std::string themeTrim(const std::string& s) { size_t b = 0; while (b < s.size() && std::isspace(static_cast(s[b]))) { ++b; } size_t e = s.size(); while (e > b && std::isspace(static_cast(s[e - 1]))) { --e; } if (e >= b + 2 && ((s[b] == '"' && s[e - 1] == '"') || (s[b] == '\'' && s[e - 1] == '\''))) { return s.substr(b + 1, e - b - 2); } return s.substr(b, e - b); } inline int themeHexNibble(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); return -1; } inline bool parseHexRgb(const std::string& raw, uint32_t* outRgb) { std::string s = themeTrim(raw); if (s.rfind("0x", 0) == 0 || s.rfind("0X", 0) == 0) { s = "#" + s.substr(2); } if (s.size() == 7 && s[0] == '#') { uint32_t v = 0; for (int i = 1; i < 7; ++i) { int n = themeHexNibble(s[i]); if (n < 0) return false; v = (v << 4) | static_cast(n); } *outRgb = v; return true; } if (s.size() == 4 && s[0] == '#') { int r = themeHexNibble(s[1]); int g = themeHexNibble(s[2]); int b = themeHexNibble(s[3]); if (r < 0 || g < 0 || b < 0) return false; *outRgb = static_cast(((r * 17) << 16) | ((g * 17) << 8) | (b * 17)); return true; } return false; } inline std::string formatHexRgb(uint32_t rgb) { char buf[16]; std::snprintf(buf, sizeof(buf), "#%06x", static_cast(rgb & 0xffffffu)); return buf; } inline uint32_t adjustRgb(uint32_t rgb, int delta) { int r = std::clamp(static_cast((rgb >> 16) & 0xff) + delta, 0, 255); int g = std::clamp(static_cast((rgb >> 8) & 0xff) + delta, 0, 255); int b = std::clamp(static_cast(rgb & 0xff) + delta, 0, 255); return static_cast((r << 16) | (g << 8) | b); } inline uint32_t blendRgb(uint32_t c1, uint32_t c2, int weight2Pct) { int w2 = std::clamp(weight2Pct, 0, 100); int w1 = 100 - w2; int r = (((c1 >> 16) & 0xff) * w1 + ((c2 >> 16) & 0xff) * w2) / 100; int g = (((c1 >> 8) & 0xff) * w1 + ((c2 >> 8) & 0xff) * w2) / 100; int b = ((c1 & 0xff) * w1 + (c2 & 0xff) * w2) / 100; return static_cast((std::clamp(r, 0, 255) << 16) | (std::clamp(g, 0, 255) << 8) | std::clamp(b, 0, 255)); } inline std::string getHomeDirPath() { const char* h = std::getenv("HOME"); return (h && h[0]) ? h : "/home/user"; } inline std::string getXresourcesPath() { return getHomeDirPath() + "/.Xresources"; } inline std::string getThemesDirPath() { return getHomeDirPath() + "/.xthemes"; } struct ThemeSpec { std::string name = "Default"; std::string fontFace = "Fira Code"; std::string iconFontFace = "DejaVuSansM Nerd Font"; int baseFontSize = 15; // Core colors (24-bit RGB) uint32_t bgMain = 0x222222; uint32_t bgBar = 0x222222; uint32_t bgHeader = 0x1c1c1c; uint32_t bgFooter = 0x181818; uint32_t bgOverlay = 0x161616; uint32_t bgAltEven = 0x282828; uint32_t bgAltOdd = 0x2e2e2e; uint32_t bgInput = 0x1a1a1a; uint32_t bgInputFocus = 0x1b3b4b; uint32_t bgButton = 0x2a2a2a; uint32_t bgSearch = 0x383838; uint32_t bgBubbleIn = 0x262d32; uint32_t bgBubbleOut = 0x1f2d38; uint32_t bgToast = 0x102535; uint32_t fgMain = 0xdddddd; uint32_t fgBright = 0xffffff; uint32_t fgMuted = 0x999999; uint32_t selBg = 0x005577; uint32_t selFg = 0xffffff; uint32_t accent = 0xf4d03f; uint32_t accentAlt = 0x0088bb; uint32_t highlight = 0xffc978; uint32_t border = 0x444444; uint32_t divider = 0x666666; uint32_t success = 0x2ecc71; uint32_t info = 0x5dade2; uint32_t warning = 0xe67e22; uint32_t danger = 0xe74c3c; uint32_t dangerBg = 0x922b21; int sizeSmall() const { return std::max(8, static_cast(std::lround(baseFontSize * 12.0 / 15.0))); } int sizeMain() const { return std::clamp(baseFontSize, 10, 24); } int sizeIcon() const { return std::max(10, static_cast(std::lround(baseFontSize * 16.0 / 15.0))); } int sizeLarge() const { return std::max(11, static_cast(std::lround(baseFontSize * 18.0 / 15.0))); } std::string pangoMainFontStr() const { return fontFace + ", " + iconFontFace + " " + std::to_string(sizeMain()) + "px"; } std::string pangoSmallFontStr() const { return fontFace + ", " + iconFontFace + " " + std::to_string(sizeSmall()) + "px"; } std::string pangoIconFontStr() const { return iconFontFace + ", " + fontFace + " " + std::to_string(sizeIcon()) + "px"; } std::string pangoLargeBoldFontStr() const { return fontFace + ", " + iconFontFace + " Bold " + std::to_string(sizeLarge()) + "px"; } std::string xftFontStr(int pxSize) const { return fontFace + ":pixelsize=" + std::to_string(pxSize) + ":style=Regular:antialias=true:autohint=true"; } std::string xftIconFontStr(int pxSize) const { return iconFontFace + ":pixelsize=" + std::to_string(pxSize) + ":style=Regular:antialias=true:autohint=true"; } void recomputeDerivedFromCore() { int luma = (((bgMain >> 16) & 0xff) * 299 + ((bgMain >> 8) & 0xff) * 587 + (bgMain & 0xff) * 114) / 1000; int dir = (luma > 140) ? -1 : 1; bgBar = bgMain; bgHeader = adjustRgb(bgMain, -dir * 6); bgFooter = adjustRgb(bgMain, -dir * 10); bgOverlay = adjustRgb(bgMain, -dir * 12); bgAltEven = adjustRgb(bgMain, dir * 6); bgAltOdd = adjustRgb(bgMain, dir * 12); bgInput = adjustRgb(bgMain, -dir * 8); bgInputFocus = blendRgb(bgMain, selBg, 40); bgButton = adjustRgb(bgMain, dir * 8); bgSearch = adjustRgb(bgMain, dir * 22); bgBubbleIn = blendRgb(bgMain, success, 14); bgBubbleOut = blendRgb(bgMain, selBg, 26); bgToast = blendRgb(bgOverlay, selBg, 35); int fgLuma = (((fgMain >> 16) & 0xff) * 299 + ((fgMain >> 8) & 0xff) * 587 + (fgMain & 0xff) * 114) / 1000; fgBright = (fgLuma > 128) ? 0xffffff : 0x000000; fgMuted = blendRgb(fgMain, bgMain, 45); accentAlt = adjustRgb(selBg, 48); highlight = blendRgb(accent, 0xffffff, 30); border = adjustRgb(bgMain, dir * 34); divider = adjustRgb(bgMain, dir * 68); } void parseFromMap(const std::unordered_map& kv) { auto getStr = [&](const char* k1, const char* k2 = nullptr, const char* k3 = nullptr) -> std::string { auto it = kv.find(k1); if (it != kv.end() && !it->second.empty()) return it->second; if (k2) { it = kv.find(k2); if (it != kv.end() && !it->second.empty()) return it->second; } if (k3) { it = kv.find(k3); if (it != kv.end() && !it->second.empty()) return it->second; } return ""; }; std::string nm = getStr("theme.name", "*themeName"); if (!nm.empty()) name = nm; std::string ff = getStr("theme.fontFace", "*fontFace"); if (!ff.empty()) { fontFace = ff; } else { std::string dwmFont = getStr("dwm.font"); if (!dwmFont.empty()) { size_t comma = dwmFont.find(','); if (comma != std::string::npos) { fontFace = themeTrim(dwmFont.substr(0, comma)); } } } std::string iff = getStr("theme.iconFontFace", "*iconFontFace"); if (!iff.empty()) iconFontFace = iff; std::string bfs = getStr("theme.baseFontSize", "*baseFontSize"); if (!bfs.empty()) { int sz = std::atoi(bfs.c_str()); if (sz >= 10 && sz <= 24) baseFontSize = sz; } else { std::string dwmFont = getStr("dwm.font"); size_t pxPos = dwmFont.rfind("px"); if (pxPos != std::string::npos && pxPos > 0) { size_t sp = pxPos; while (sp > 0 && std::isdigit(static_cast(dwmFont[sp - 1]))) { --sp; } if (sp < pxPos) { int sz = std::atoi(dwmFont.substr(sp, pxPos - sp).c_str()); if (sz >= 10 && sz <= 24) baseFontSize = sz; } } } auto getCol = [&](uint32_t* dst, const char* k1, const char* k2 = nullptr, const char* k3 = nullptr) -> bool { std::string s = getStr(k1, k2, k3); if (s.empty()) return false; uint32_t v = 0; if (parseHexRgb(s, &v)) { *dst = v; return true; } return false; }; bool hasCoreBg = getCol(&bgMain, "theme.bgMain", "*background", "dmenu.background"); bool hasCoreFg = getCol(&fgMain, "theme.fgMain", "*foreground", "dwm.normfgcolor"); bool hasSelBg = getCol(&selBg, "theme.selBg", "dmenu.selbackground", "dwm.selbgcolor"); bool hasSelFg = getCol(&selFg, "theme.selFg", "dmenu.selforeground", "dwm.selfgcolor"); bool hasAccent = getCol(&accent, "theme.accent", "*accentColor", "st.cursorColor"); (void)hasCoreBg; (void)hasCoreFg; (void)hasSelBg; (void)hasSelFg; (void)hasAccent; // Derive defaults from core colors first, then override with any explicit tokens recomputeDerivedFromCore(); getCol(&bgBar, "theme.bgBar", "dwm.background", "dwm.normbgcolor"); getCol(&bgHeader, "theme.bgHeader", "dmenu.background", "st.background"); getCol(&bgFooter, "theme.bgFooter"); getCol(&bgOverlay, "theme.bgOverlay"); getCol(&bgAltEven, "theme.bgAltEven"); getCol(&bgAltOdd, "theme.bgAltOdd"); getCol(&bgInput, "theme.bgInput"); getCol(&bgInputFocus, "theme.bgInputFocus"); getCol(&bgButton, "theme.bgButton"); getCol(&bgSearch, "theme.bgSearch", "dmenu.outbackground"); getCol(&bgBubbleIn, "theme.bgBubbleIn"); getCol(&bgBubbleOut, "theme.bgBubbleOut"); getCol(&bgToast, "theme.bgToast"); getCol(&fgBright, "theme.fgBright", "dmenu.selforeground"); getCol(&fgMuted, "theme.fgMuted"); getCol(&accentAlt, "theme.accentAlt"); getCol(&highlight, "theme.highlight", "dmenu.highlight"); getCol(&border, "theme.border", "dwm.border", "dwm.normbordercolor"); getCol(÷r, "theme.divider"); getCol(&success, "theme.success"); getCol(&info, "theme.info"); getCol(&warning, "theme.warning"); getCol(&danger, "theme.danger"); getCol(&dangerBg, "theme.dangerBg"); } static void parseLinesIntoMap(const std::string& content, std::unordered_map& kv) { std::istringstream ss(content); std::string line; while (std::getline(ss, line)) { std::string t = themeTrim(line); if (t.empty() || t[0] == '!' || t[0] == '#') continue; size_t colon = t.find(':'); if (colon == std::string::npos) continue; std::string key = themeTrim(t.substr(0, colon)); std::string val = themeTrim(t.substr(colon + 1)); if (!key.empty() && !val.empty()) { kv[key] = val; } } } bool loadFromFile(const std::string& path) { std::ifstream in(path); if (!in.is_open()) return false; std::ostringstream ss; ss << in.rdbuf(); std::unordered_map kv; parseLinesIntoMap(ss.str(), kv); parseFromMap(kv); return true; } void loadActive(Display* dpy, int screen) { std::unordered_map kv; if (dpy) { Atom actualType = None; int actualFormat = 0; unsigned long nitems = 0, bytesAfter = 0; unsigned char* prop = nullptr; if (XGetWindowProperty(dpy, RootWindow(dpy, screen), XA_RESOURCE_MANAGER, 0L, 65536L, False, XA_STRING, &actualType, &actualFormat, &nitems, &bytesAfter, &prop) == Success && prop) { if (actualType == XA_STRING && actualFormat == 8 && nitems > 0) { std::string resStr(reinterpret_cast(prop), nitems); parseLinesIntoMap(resStr, kv); } XFree(prop); } } // Also read ~/.Xresources so direct file edits always take effect std::ifstream in(getXresourcesPath()); if (in.is_open()) { std::ostringstream ss; ss << in.rdbuf(); parseLinesIntoMap(ss.str(), kv); } if (!kv.empty()) { parseFromMap(kv); } } std::string serializeXresources() const { std::ostringstream out; out << "! Space to setup configversion\n"; out << "! configversion: 7bff124b2de16292fe6e7a7e5d3322ae\n"; out << "! Bananarchy Unified Theme: " << name << "\n\n"; out << "theme.name: " << name << "\n"; out << "theme.fontFace: " << fontFace << "\n"; out << "theme.iconFontFace: " << iconFontFace << "\n"; out << "theme.baseFontSize: " << sizeMain() << "\n\n"; out << "theme.bgMain: " << formatHexRgb(bgMain) << "\n"; out << "theme.bgBar: " << formatHexRgb(bgBar) << "\n"; out << "theme.bgHeader: " << formatHexRgb(bgHeader) << "\n"; out << "theme.bgFooter: " << formatHexRgb(bgFooter) << "\n"; out << "theme.bgOverlay: " << formatHexRgb(bgOverlay) << "\n"; out << "theme.bgAltEven: " << formatHexRgb(bgAltEven) << "\n"; out << "theme.bgAltOdd: " << formatHexRgb(bgAltOdd) << "\n"; out << "theme.bgInput: " << formatHexRgb(bgInput) << "\n"; out << "theme.bgInputFocus: " << formatHexRgb(bgInputFocus) << "\n"; out << "theme.bgButton: " << formatHexRgb(bgButton) << "\n"; out << "theme.bgSearch: " << formatHexRgb(bgSearch) << "\n"; out << "theme.bgBubbleIn: " << formatHexRgb(bgBubbleIn) << "\n"; out << "theme.bgBubbleOut: " << formatHexRgb(bgBubbleOut) << "\n"; out << "theme.bgToast: " << formatHexRgb(bgToast) << "\n\n"; out << "theme.fgMain: " << formatHexRgb(fgMain) << "\n"; out << "theme.fgBright: " << formatHexRgb(fgBright) << "\n"; out << "theme.fgMuted: " << formatHexRgb(fgMuted) << "\n\n"; out << "theme.selBg: " << formatHexRgb(selBg) << "\n"; out << "theme.selFg: " << formatHexRgb(selFg) << "\n"; out << "theme.accent: " << formatHexRgb(accent) << "\n"; out << "theme.accentAlt: " << formatHexRgb(accentAlt) << "\n"; out << "theme.highlight: " << formatHexRgb(highlight) << "\n"; out << "theme.border: " << formatHexRgb(border) << "\n"; out << "theme.divider: " << formatHexRgb(divider) << "\n\n"; out << "theme.success: " << formatHexRgb(success) << "\n"; out << "theme.info: " << formatHexRgb(info) << "\n"; out << "theme.warning: " << formatHexRgb(warning) << "\n"; out << "theme.danger: " << formatHexRgb(danger) << "\n"; out << "theme.dangerBg: " << formatHexRgb(dangerBg) << "\n\n"; out << "dmenu.font: " << xftFontStr(sizeLarge()) << "\n"; out << "dmenu.background: " << formatHexRgb(bgHeader) << "\n"; out << "dmenu.foreground: " << formatHexRgb(fgBright) << "\n"; out << "dmenu.selbackground: " << formatHexRgb(selBg) << "\n"; out << "dmenu.selforeground: " << formatHexRgb(selFg) << "\n"; out << "dmenu.outbackground: " << formatHexRgb(bgSearch) << "\n"; out << "dmenu.outforeground: " << formatHexRgb(fgBright) << "\n"; out << "dmenu.highlight: " << formatHexRgb(highlight) << "\n\n"; out << "dwm.font: " << pangoMainFontStr() << "\n"; out << "dwm.background: " << formatHexRgb(bgBar) << "\n"; out << "dwm.foreground: " << formatHexRgb(fgMain) << "\n"; out << "dwm.border: " << formatHexRgb(border) << "\n"; out << "dwm.selbackground: " << formatHexRgb(selBg) << "\n"; out << "dwm.selforeground: " << formatHexRgb(selFg) << "\n"; out << "dwm.selborder: " << formatHexRgb(selBg) << "\n"; out << "dwm.normbgcolor: " << formatHexRgb(bgBar) << "\n"; out << "dwm.normbordercolor: " << formatHexRgb(border) << "\n"; out << "dwm.normfgcolor: " << formatHexRgb(fgMain) << "\n"; out << "dwm.selbgcolor: " << formatHexRgb(selBg) << "\n"; out << "dwm.selbordercolor: " << formatHexRgb(selBg) << "\n"; out << "dwm.selfgcolor: " << formatHexRgb(selFg) << "\n\n"; out << "st.font: " << xftFontStr(sizeLarge()) << "\n"; out << "st.termname: xterm-256color\n"; out << "st.background: " << formatHexRgb(bgHeader) << "\n"; out << "st.foreground: " << formatHexRgb(fgMain) << "\n"; out << "st.cursorColor: " << formatHexRgb(accent) << "\n\n"; out << "svkbd.font: " << fontFace << ":pixelsize=" << sizeSmall() << "\n"; return out.str(); } bool saveToFile(const std::string& path) const { std::ofstream out(path, std::ios::trunc); if (!out.is_open()) return false; out << serializeXresources(); return true; } }; inline ThemeSpec loadThemeSpec(Display* dpy = nullptr, int screen = 0) { ThemeSpec spec; spec.loadActive(dpy, screen); return spec; } struct PluginTheme { ThemeSpec spec; time_t lastXresMtime = -1; off_t lastXresSize = -1; bool colorsAllocated = false; Display* dpy_ = nullptr; int screen_ = 0; Visual* visual_ = nullptr; Colormap cmap_ = 0; Window win_ = None; PangoContext* pangoCtx = nullptr; PangoLayout* layoutMain = nullptr; PangoLayout* layoutSmall = nullptr; PangoLayout* layoutIcon = nullptr; PangoLayout* layoutNum = nullptr; XftColor colWhite{}; XftColor colLight{}; XftColor colMuted{}; XftColor colYellow{}; XftColor colGreen{}; XftColor colBlue{}; XftColor colOrange{}; XftColor colRed{}; void allocXft(Display* dpy, Visual* visual, Colormap cmap, uint32_t rgb, XftColor* dst) { std::string hex = formatHexRgb(rgb); XftColorAllocName(dpy, visual, cmap, hex.c_str(), dst); } void init(Display* dpy, int screen, Visual* visual, Colormap cmap, Window win = None) { dpy_ = dpy; screen_ = screen; visual_ = visual; cmap_ = cmap; win_ = win; if (!pangoCtx && dpy) { PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen); pangoCtx = pango_font_map_create_context(fontMap); layoutMain = pango_layout_new(pangoCtx); layoutSmall = pango_layout_new(pangoCtx); layoutIcon = pango_layout_new(pangoCtx); layoutNum = pango_layout_new(pangoCtx); } reload(); } void reload() { if (dpy_ && visual_) { load(dpy_, screen_, visual_, cmap_, layoutMain, layoutSmall, layoutIcon, layoutNum, win_); } } void load(Display* dpy, int screen, Visual* visual, Colormap cmap, PangoLayout* lMain = nullptr, PangoLayout* lSmall = nullptr, PangoLayout* lIcon = nullptr, PangoLayout* lNum = nullptr, Window win = None) { dpy_ = dpy; screen_ = screen; visual_ = visual; cmap_ = cmap; if (win != None) win_ = win; if (lMain) layoutMain = lMain; if (lSmall) layoutSmall = lSmall; if (lIcon) layoutIcon = lIcon; if (lNum) layoutNum = lNum; struct stat st {}; if (::stat(getXresourcesPath().c_str(), &st) == 0) { lastXresMtime = st.st_mtim.tv_sec; lastXresSize = st.st_size; } spec = ThemeSpec{}; spec.loadActive(dpy, screen); if (colorsAllocated) { XftColorFree(dpy, visual, cmap, &colWhite); XftColorFree(dpy, visual, cmap, &colLight); XftColorFree(dpy, visual, cmap, &colMuted); XftColorFree(dpy, visual, cmap, &colYellow); XftColorFree(dpy, visual, cmap, &colGreen); XftColorFree(dpy, visual, cmap, &colBlue); XftColorFree(dpy, visual, cmap, &colOrange); XftColorFree(dpy, visual, cmap, &colRed); } allocXft(dpy, visual, cmap, spec.fgBright, &colWhite); allocXft(dpy, visual, cmap, spec.fgMain, &colLight); allocXft(dpy, visual, cmap, spec.fgMuted, &colMuted); allocXft(dpy, visual, cmap, spec.accent, &colYellow); allocXft(dpy, visual, cmap, spec.success, &colGreen); allocXft(dpy, visual, cmap, spec.info, &colBlue); allocXft(dpy, visual, cmap, spec.warning, &colOrange); allocXft(dpy, visual, cmap, spec.danger, &colRed); colorsAllocated = true; if (layoutMain) { PangoFontDescription* d = pango_font_description_from_string(spec.pangoMainFontStr().c_str()); pango_layout_set_font_description(layoutMain, d); pango_font_description_free(d); } if (layoutSmall) { PangoFontDescription* d = pango_font_description_from_string(spec.pangoSmallFontStr().c_str()); pango_layout_set_font_description(layoutSmall, d); pango_font_description_free(d); } if (layoutIcon) { PangoFontDescription* d = pango_font_description_from_string(spec.pangoIconFontStr().c_str()); pango_layout_set_font_description(layoutIcon, d); pango_font_description_free(d); } if (layoutNum) { PangoFontDescription* d = pango_font_description_from_string( spec.pangoLargeBoldFontStr().c_str()); pango_layout_set_font_description(layoutNum, d); pango_font_description_free(d); } if (win_ != None) { XSetWindowBackground(dpy, win_, spec.bgMain); XSetWindowBorder(dpy, win_, spec.selBg); } } bool checkAndReload() { if (!dpy_ || !visual_) return false; return checkAndReload(dpy_, screen_, visual_, cmap_, layoutMain, layoutSmall, layoutIcon, layoutNum, win_); } bool checkAndReload(Display* dpy, int screen, Visual* visual, Colormap cmap, PangoLayout* lMain = nullptr, PangoLayout* lSmall = nullptr, PangoLayout* lIcon = nullptr, PangoLayout* lNum = nullptr, Window win = None) { struct stat st {}; if (::stat(getXresourcesPath().c_str(), &st) == 0) { if (st.st_mtim.tv_sec != lastXresMtime || st.st_size != lastXresSize) { load(dpy, screen, visual, cmap, lMain, lSmall, lIcon, lNum, win); return true; } } return false; } }; } // namespace bananarchy #endif // DWM_PLUGINS_THEME_H