From e98f6934c728748f7eee0af0f29d40cedeb97828 Mon Sep 17 00:00:00 2001 From: nguyenphivn Date: Mon, 14 Sep 2026 08:17:02 +0700 Subject: [PATCH 1/3] fix(icon): pick the tray icon colour from the KDE Plasma panel (#374) On KDE the settings portal reports the application colour scheme (kdeglobals), but the tray sits on the panel, which the Plasma Style paints. The default Fedora 44 look pairs BreezeLight with the breeze-dark Plasma Style (stored in ~/.config/kdedefaults), so Auto picked the black icon on a black panel. When XDG_CURRENT_DESKTOP names KDE, read plasmarc through the KConfig cascade, use the theme's own colors file when it ships one and the system scheme otherwise, and apply the portal's qGray < 192 threshold. Plain file reads, no subprocess; fall back to the old probes when no colour is found. Verified on Fedora 44 KDE Wayland: tray icon black before, white after. New unit test plasma_panel_theme: 15 cases, 12 red on an empty stub as declared beforehand, all green with the fix; ctest 10/10 on dev. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01W39vFnx4ECponJC5MQ9Fiu --- CMakeLists.txt | 1 + src/lotus-engine.cpp | 11 ++ src/lotus-plasma-theme.cpp | 198 ++++++++++++++++++++++++++++++++++++ src/lotus-plasma-theme.h | 65 ++++++++++++ test/CMakeLists.txt | 13 +++ test/plasma-panel-theme.cpp | 175 +++++++++++++++++++++++++++++++ 6 files changed, 463 insertions(+) create mode 100644 src/lotus-plasma-theme.cpp create mode 100644 src/lotus-plasma-theme.h create mode 100644 test/plasma-panel-theme.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 92916579..c23d4530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ set(fcitx_lotus_core_sources ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-monitor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-icon-resolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-plasma-theme.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/emoji.cpp ) diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index 72857bf1..a4fd16cc 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -15,6 +15,7 @@ #include "lotus-utils.h" #include "lotus-icon-resolver.h" #include "ack-apps.h" +#include "lotus-plasma-theme.h" #include #include #include @@ -108,6 +109,16 @@ namespace fcitx { lastCheckMs = now; cachedValue = false; + // KDE Plasma: the tray sits on the panel, painted by the Plasma Style, + // while the portal below reports the application colour scheme. The + // two differ in the default Fedora/Kubuntu look (#374). + if (isKdePlasmaSession(std::getenv("XDG_CURRENT_DESKTOP"))) { + if (const auto dark = isPlasmaPanelDark(plasmaThemeSearchPathsFromEnv())) { + cachedValue = *dark; + return cachedValue; + } + } + // GTK_THEME is honored by lightweight DEs that lack the settings // portal; covers XFCE, openbox, etc. with a dark theme. if (const char* theme = std::getenv("GTK_THEME")) { diff --git a/src/lotus-plasma-theme.cpp b/src/lotus-plasma-theme.cpp new file mode 100644 index 00000000..ccd1116c --- /dev/null +++ b/src/lotus-plasma-theme.cpp @@ -0,0 +1,198 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "lotus-plasma-theme.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace fcitx { + + static std::string trim(const std::string& s) { + const auto begin = s.find_first_not_of(" \t\r"); + if (begin == std::string::npos) + return ""; + const auto end = s.find_last_not_of(" \t\r"); + return s.substr(begin, end - begin + 1); + } + + static std::string joinPath(const std::string& dir, const std::string& name) { + if (dir.empty()) + return name; + if (dir.back() == '/') + return dir + name; + return dir + "/" + name; + } + + static bool isDirectory(const std::string& path) { + struct stat st{}; + return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode); + } + + static std::vector splitPathList(const std::string& list) { + std::vector result; + std::stringstream ss(list); + std::string item; + while (std::getline(ss, item, ':')) { + if (!item.empty()) + result.push_back(item); + } + return result; + } + + static std::string envOr(const char* name, const std::string& fallback) { + const char* value = std::getenv(name); + return (value != nullptr && *value != '\0') ? std::string(value) : fallback; + } + + // Reads "key=value" from "[group]" of a KConfig-style INI file. + static std::optional readIniValue(const std::string& path, const std::string& group, const std::string& key) { + std::ifstream file(path); + if (!file.is_open()) + return std::nullopt; + + const std::string header = "[" + group + "]"; + bool inGroup = false; + std::string line; + while (std::getline(file, line)) { + line = trim(line); + if (line.empty() || line[0] == '#') + continue; + if (line[0] == '[') { + // "[Theme][$i]" marks an immutable group; same group. + inGroup = line == header || line == header + "[$i]"; + continue; + } + if (!inGroup) + continue; + const auto eq = line.find('='); + if (eq != std::string::npos && trim(line.substr(0, eq)) == key) + return trim(line.substr(eq + 1)); + } + return std::nullopt; + } + + // KConfig cascade: the user file wins over system files. + static std::optional readConfigValue(const PlasmaThemeSearchPaths& paths, const std::string& fileName, const std::string& group, const std::string& key) { + if (auto value = readIniValue(joinPath(paths.configHome, fileName), group, key)) + return value; + for (const auto& dir : paths.configDirs) { + if (auto value = readIniValue(joinPath(dir, fileName), group, key)) + return value; + } + return std::nullopt; + } + + static std::vector dataSearchDirs(const PlasmaThemeSearchPaths& paths) { + std::vector dirs; + if (!paths.dataHome.empty()) + dirs.push_back(paths.dataHome); + dirs.insert(dirs.end(), paths.dataDirs.begin(), paths.dataDirs.end()); + return dirs; + } + + // Parses "r,g,b" (an optional fourth alpha component is ignored). + static std::optional> parseRgb(const std::string& value) { + std::array rgb{}; + std::stringstream ss(value); + std::string part; + size_t count = 0; + while (std::getline(ss, part, ',')) { + part = trim(part); + if (part.empty() || part.size() > 3 || !std::all_of(part.begin(), part.end(), [](char c) { return c >= '0' && c <= '9'; })) + return std::nullopt; + const int component = std::stoi(part); + if (component > 255) + return std::nullopt; + if (count < rgb.size()) + rgb[count] = component; + ++count; + } + if (count != 3 && count != 4) + return std::nullopt; + return rgb; + } + + // Same rule as xdg-desktop-portal-kde: qGray(background) < 192 is dark. + static std::optional isDarkBackground(const std::optional& value) { + if (!value) + return std::nullopt; + const auto rgb = parseRgb(*value); + if (!rgb) + return std::nullopt; + const int gray = ((*rgb)[0] * 11 + (*rgb)[1] * 16 + (*rgb)[2] * 5) / 32; + return gray < 192; + } + + static std::optional isDarkColorsFile(const std::string& path) { + return isDarkBackground(readIniValue(path, "Colors:Window", "BackgroundNormal")); + } + + static std::optional isDarkSystemScheme(const PlasmaThemeSearchPaths& paths) { + if (auto dark = isDarkBackground(readConfigValue(paths, "kdeglobals", "Colors:Window", "BackgroundNormal"))) + return dark; + + // kdeglobals may only name the scheme; its colours live in a data file. + const auto scheme = readConfigValue(paths, "kdeglobals", "General", "ColorScheme"); + if (!scheme || scheme->empty()) + return std::nullopt; + for (const auto& dir : dataSearchDirs(paths)) { + const std::string file = joinPath(dir, "color-schemes/" + *scheme + ".colors"); + if (access(file.c_str(), R_OK) == 0) + return isDarkColorsFile(file); + } + return std::nullopt; + } + + PlasmaThemeSearchPaths plasmaThemeSearchPathsFromEnv() { + const std::string home = envOr("HOME", ""); + + PlasmaThemeSearchPaths paths; + paths.configHome = envOr("XDG_CONFIG_HOME", joinPath(home, ".config")); + paths.configDirs = splitPathList(envOr("XDG_CONFIG_DIRS", "/etc/xdg")); + paths.dataHome = envOr("XDG_DATA_HOME", joinPath(home, ".local/share")); + paths.dataDirs = splitPathList(envOr("XDG_DATA_DIRS", "/usr/local/share:/usr/share")); + + // Plasma's session startup puts kdedefaults first in XDG_CONFIG_DIRS; + // add it when fcitx5 was started with a different environment. + const std::string kdeDefaults = joinPath(paths.configHome, "kdedefaults"); + if (std::find(paths.configDirs.begin(), paths.configDirs.end(), kdeDefaults) == paths.configDirs.end()) + paths.configDirs.insert(paths.configDirs.begin(), kdeDefaults); + return paths; + } + + bool isKdePlasmaSession(const char* xdgCurrentDesktop) { + if (xdgCurrentDesktop == nullptr) + return false; + const auto desktops = splitPathList(xdgCurrentDesktop); + return std::find(desktops.begin(), desktops.end(), "KDE") != desktops.end(); + } + + std::optional isPlasmaPanelDark(const PlasmaThemeSearchPaths& paths) { + auto theme = readConfigValue(paths, "plasmarc", "Theme", "name"); + if (!theme || theme->empty()) + theme = "default"; + + // Plasma uses the first installed copy of the theme. A theme with its + // own colors file paints the panel with it; one without (Breeze + // "default") follows the system colour scheme. + for (const auto& dir : dataSearchDirs(paths)) { + const std::string themeDir = joinPath(dir, "plasma/desktoptheme/" + *theme); + if (!isDirectory(themeDir)) + continue; + const std::string colors = joinPath(themeDir, "colors"); + if (access(colors.c_str(), R_OK) == 0) + return isDarkColorsFile(colors); + break; + } + return isDarkSystemScheme(paths); + } + +} // namespace fcitx diff --git a/src/lotus-plasma-theme.h b/src/lotus-plasma-theme.h new file mode 100644 index 00000000..ba95a2e7 --- /dev/null +++ b/src/lotus-plasma-theme.h @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +/** + * @file lotus-plasma-theme.h + * @brief Detects whether the KDE Plasma panel is dark. + * + * KDE keeps two independent colour settings: the application colour scheme + * (kdeglobals) and the Plasma Style that paints the panel (plasmarc). The + * settings portal only reports the former, so the default Fedora/Kubuntu + * look — light applications on a dark panel — picked a black tray icon on a + * black panel (issue #374). The tray lives on the panel, so the panel's + * colours are the ones that matter. + * + * Only plain config/data files are read; no subprocess is spawned. + */ + +#ifndef _FCITX5_LOTUS_PLASMA_THEME_H_ +#define _FCITX5_LOTUS_PLASMA_THEME_H_ + +#include +#include +#include + +namespace fcitx { + + struct PlasmaThemeSearchPaths { + // Config directories searched in KConfig cascade order: the user + // directory first, then each system directory. + std::string configHome; + std::vector configDirs; + // Data directories holding plasma/desktoptheme and color-schemes. + std::string dataHome; + std::vector dataDirs; + }; + + /** + * @brief Builds search paths from the XDG environment variables. + * + * Adds "/kdedefaults" (where Plasma 6 stores the Global + * Theme defaults) when the environment does not list it already. + */ + PlasmaThemeSearchPaths plasmaThemeSearchPathsFromEnv(); + + /** + * @brief Tells whether XDG_CURRENT_DESKTOP names KDE. + */ + bool isKdePlasmaSession(const char* xdgCurrentDesktop); + + /** + * @brief Reports whether the Plasma panel background is dark. + * + * Uses the Plasma Style's own colours when it ships a colors file, and + * the system colour scheme otherwise (Breeze "default" follows it). + * + * @return true for a dark panel, false for a light one, std::nullopt + * when no colour could be found. + */ + std::optional isPlasmaPanelDark(const PlasmaThemeSearchPaths& paths); + +} // namespace fcitx + +#endif // _FCITX5_LOTUS_PLASMA_THEME_H_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e53f030f..f63b3291 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -81,3 +81,16 @@ set_tests_properties(emoji_backspace_utf8 PROPERTIES LABELS "unit;emoji" TIMEOUT 20 ) + +add_executable(plasma_panel_theme + plasma-panel-theme.cpp + ${PROJECT_SOURCE_DIR}/src/lotus-plasma-theme.cpp +) +target_include_directories(plasma_panel_theme PRIVATE + ${PROJECT_SOURCE_DIR}/src +) +add_test(NAME plasma_panel_theme COMMAND plasma_panel_theme) +set_tests_properties(plasma_panel_theme PROPERTIES + LABELS "unit;icon" + TIMEOUT 20 +) diff --git a/test/plasma-panel-theme.cpp b/test/plasma-panel-theme.cpp new file mode 100644 index 00000000..43942196 --- /dev/null +++ b/test/plasma-panel-theme.cpp @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// Unit regression test for isPlasmaPanelDark() (issue #374). +// +// Each case builds a throwaway XDG tree (config + data directories) that +// mirrors a real KDE setup, then asks whether the panel is dark. The first +// case is the reported bug: the default Fedora 44 look ships a light +// application colour scheme and the breeze-dark Plasma Style, stored in +// ~/.config/kdedefaults, so the tray icon must be the white one. +// +#include "lotus-plasma-theme.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + + namespace fs = std::filesystem; + + const char* const kDark = "32,35,38"; // Breeze Dark window background + const char* const kLight = "239,240,241"; // Breeze Light window background + + int failures = 0; + + void writeFile(const fs::path& path, const std::string& content) { + fs::create_directories(path.parent_path()); + std::ofstream(path) << content; + } + + std::string colorsFile(const char* background) { + return std::string("[Colors:Window]\nBackgroundAlternate=1,2,3\nBackgroundNormal=") + background + "\nForegroundNormal=9,9,9\n"; + } + + std::string show(std::optional value) { + if (!value) + return "nullopt"; + return *value ? "dark" : "light"; + } + + void check(const char* name, bool ok, const std::string& detail) { + std::printf("%s %s%s%s\n", ok ? "PASS" : "FAIL", name, ok ? "" : " — ", ok ? "" : detail.c_str()); + if (!ok) + ++failures; + } + + // One isolated XDG tree per case. + struct Tree { + fs::path root; + fcitx::PlasmaThemeSearchPaths paths; + + explicit Tree(const char* name) : root(fs::temp_directory_path() / ("lotus-plasma-theme-test-" + std::to_string(getpid()) + "-" + name)) { + fs::remove_all(root); + paths.configHome = (root / "config").string(); + paths.configDirs = {(root / "config/kdedefaults").string(), (root / "etc/xdg").string()}; + paths.dataHome = (root / "local/share").string(); + paths.dataDirs = {(root / "usr/share").string()}; + // Themes installed on every KDE system. + writeFile(root / "usr/share/plasma/desktoptheme/default/metadata.json", "{}"); + writeFile(root / "usr/share/plasma/desktoptheme/breeze-dark/colors", colorsFile(kDark)); + writeFile(root / "usr/share/plasma/desktoptheme/breeze-light/colors", colorsFile(kLight)); + } + ~Tree() { + fs::remove_all(root); + } + void write(const char* relative, const std::string& content) { + writeFile(root / relative, content); + } + }; + + void expect(const char* name, Tree& tree, std::optional expected) { + const auto actual = fcitx::isPlasmaPanelDark(tree.paths); + check(name, actual == expected, "expected " + show(expected) + ", got " + show(actual)); + } + +} // namespace + +int main() { + { + // The #374 report: light apps, dark panel, set by the Global Theme. + Tree t("fedora-default"); + t.write("config/kdedefaults/plasmarc", "[Theme]\nname=breeze-dark\n"); + t.write("config/kdeglobals", colorsFile(kLight)); + expect("P1 fedora_default_light_apps_dark_panel", t, true); + } + { + Tree t("breeze-light-scheme"); + t.write("config/plasmarc", "[Theme]\nname=default\n"); + t.write("config/kdeglobals", colorsFile(kLight)); + expect("P2 breeze_default_follows_light_scheme", t, false); + } + { + Tree t("breeze-dark-scheme"); + t.write("config/plasmarc", "[Theme]\nname=default\n"); + t.write("config/kdeglobals", colorsFile(kDark)); + expect("P3 breeze_default_follows_dark_scheme", t, true); + } + { + Tree t("light-panel-dark-apps"); + t.write("config/plasmarc", "[Theme]\nname=breeze-light\n"); + t.write("config/kdeglobals", colorsFile(kDark)); + expect("P4 light_panel_with_dark_apps", t, false); + } + { + Tree t("user-overrides-defaults"); + t.write("config/kdedefaults/plasmarc", "[Theme]\nname=breeze-dark\n"); + t.write("config/plasmarc", "[Theme]\nname=breeze-light\n"); + t.write("config/kdeglobals", colorsFile(kDark)); + expect("P5 user_plasmarc_overrides_global_theme", t, false); + } + { + Tree t("user-theme-shadows-system"); + t.write("config/plasmarc", "[Theme]\nname=breeze-light\n"); + t.write("local/share/plasma/desktoptheme/breeze-light/colors", colorsFile(kDark)); + t.write("config/kdeglobals", colorsFile(kLight)); + expect("P6 user_installed_theme_shadows_system", t, true); + } + { + // A user copy without a colors file follows the scheme, even though + // the system copy of the same name has dark colours. + Tree t("user-theme-no-colors"); + t.write("config/plasmarc", "[Theme]\nname=breeze-dark\n"); + t.write("local/share/plasma/desktoptheme/breeze-dark/metadata.json", "{}"); + t.write("config/kdeglobals", colorsFile(kLight)); + expect("P7 user_theme_without_colors_follows_scheme", t, false); + } + { + Tree t("missing-theme"); + t.write("config/plasmarc", "[Theme]\nname=not-installed\n"); + t.write("config/kdeglobals", colorsFile(kDark)); + expect("P8 missing_theme_follows_scheme", t, true); + } + { + Tree t("scheme-name-only"); + t.write("config/kdedefaults/kdeglobals", "[General]\nColorScheme=BreezeDark\n"); + t.write("usr/share/color-schemes/BreezeDark.colors", colorsFile(kDark)); + expect("P9 scheme_name_only_resolves_colors_file", t, true); + } + { + Tree t("nothing-known"); + expect("P10 nothing_known", t, std::nullopt); + } + + check("S1 kde_session", fcitx::isKdePlasmaSession("KDE"), "KDE not recognised"); + check("S2 gnome_session", !fcitx::isKdePlasmaSession("GNOME"), "GNOME taken for KDE"); + check("S3 no_desktop", !fcitx::isKdePlasmaSession(nullptr), "nullptr taken for KDE"); + + { + setenv("HOME", "/h", 1); + setenv("XDG_CONFIG_HOME", "/c", 1); + setenv("XDG_CONFIG_DIRS", "/etc/xdg", 1); + const auto p = fcitx::plasmaThemeSearchPathsFromEnv(); + const bool ok = p.configDirs.size() == 2 && p.configDirs[0] == "/c/kdedefaults" && p.configDirs[1] == "/etc/xdg"; + check("E1 env_adds_kdedefaults", ok, "configDirs lacks /c/kdedefaults in front of /etc/xdg"); + } + { + setenv("HOME", "/h", 1); + unsetenv("XDG_CONFIG_HOME"); + unsetenv("XDG_CONFIG_DIRS"); + unsetenv("XDG_DATA_HOME"); + unsetenv("XDG_DATA_DIRS"); + const auto p = fcitx::plasmaThemeSearchPathsFromEnv(); + const bool ok = + p.configHome == "/h/.config" && p.dataHome == "/h/.local/share" && p.dataDirs.size() == 2 && p.dataDirs[0] == "/usr/local/share" && p.dataDirs[1] == "/usr/share"; + check("E2 env_xdg_defaults", ok, "XDG defaults not applied"); + } + + std::printf("%d failure(s)\n", failures); + return failures == 0 ? 0 : 1; +} From 2b51baf7e085def26bc6595075578d4465cb53b0 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Tue, 15 Sep 2026 23:15:45 +0700 Subject: [PATCH 2/3] refactor(utils): introduce getEnv helper and clean up environment variable access --- CMakeLists.txt | 1 - src/lotus-engine.cpp | 25 ++-- src/lotus-plasma-theme.cpp | 33 ++--- src/lotus-plasma-theme.h | 2 +- src/lotus-utils.cpp | 5 + src/lotus-utils.h | 7 + test/CMakeLists.txt | 15 +-- test/emoji-history-page-navigation.cpp | 4 +- test/plasma-panel-theme.cpp | 175 ------------------------- 9 files changed, 41 insertions(+), 226 deletions(-) delete mode 100644 test/plasma-panel-theme.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c23d4530..92916579 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ set(fcitx_lotus_core_sources ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-monitor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-icon-resolver.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-plasma-theme.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/emoji.cpp ) diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index a4fd16cc..20e6b1ed 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -112,7 +112,7 @@ namespace fcitx { // KDE Plasma: the tray sits on the panel, painted by the Plasma Style, // while the portal below reports the application colour scheme. The // two differ in the default Fedora/Kubuntu look (#374). - if (isKdePlasmaSession(std::getenv("XDG_CURRENT_DESKTOP"))) { + if (isKdePlasmaSession(getEnv("XDG_CURRENT_DESKTOP"))) { if (const auto dark = isPlasmaPanelDark(plasmaThemeSearchPathsFromEnv())) { cachedValue = *dark; return cachedValue; @@ -121,10 +121,9 @@ namespace fcitx { // GTK_THEME is honored by lightweight DEs that lack the settings // portal; covers XFCE, openbox, etc. with a dark theme. - if (const char* theme = std::getenv("GTK_THEME")) { - std::string t(theme); - std::transform(t.begin(), t.end(), t.begin(), ::tolower); - if (t.find("dark") != std::string::npos) { + if (std::string theme = getEnv("GTK_THEME"); !theme.empty()) { + std::transform(theme.begin(), theme.end(), theme.begin(), ::tolower); + if (theme.find("dark") != std::string::npos) { cachedValue = true; return cachedValue; } @@ -159,8 +158,8 @@ namespace fcitx { // GTK settings file — covers DEs where the dark preference is stored // there instead of being exposed via portal/gsettings. - if (const char* home = std::getenv("HOME")) { - std::ifstream settingsFile(std::string(home) + "/.config/gtk-3.0/settings.ini"); + if (std::string home = getEnv("HOME"); !home.empty()) { + std::ifstream settingsFile(home + "/.config/gtk-3.0/settings.ini"); if (settingsFile.is_open()) { std::string line; while (std::getline(settingsFile, line)) { @@ -209,8 +208,8 @@ namespace fcitx { } LotusEngine::LotusEngine(Instance* instance) : instance_(instance), factory_([this](InputContext& ic) { return new LotusState(this, &ic); }) { //NOLINT - const char* desktop = std::getenv("XDG_CURRENT_DESKTOP"); - isGnome_ = (desktop != nullptr) && std::string(desktop).find("GNOME") != std::string::npos; + std::string desktop = getEnv("XDG_CURRENT_DESKTOP"); + isGnome_ = (!desktop.empty()) && desktop.find("GNOME") != std::string::npos; // emptyCustomKeymap_.customKeymap is implicitly initialized to empty by fcitx::Option default value macro. Init(); { @@ -1209,10 +1208,10 @@ namespace fcitx { // On KDE and GNOME, absolute paths work correctly — their compositors // or SNI hosts handle filesystem paths in IconName. static const bool kIsCinnamon = [] { - const char* de = std::getenv("XDG_CURRENT_DESKTOP"); - if (!de) - de = std::getenv("DESKTOP_SESSION"); - return de && (std::string(de) == "cinnamon" || std::string(de) == "X-Cinnamon"); + std::string de = getEnv("XDG_CURRENT_DESKTOP"); + if (de.empty()) + de = getEnv("DESKTOP_SESSION"); + return !de.empty() && (de == "cinnamon" || de == "X-Cinnamon"); }(); if (kIsCinnamon) { diff --git a/src/lotus-plasma-theme.cpp b/src/lotus-plasma-theme.cpp index ccd1116c..dbee0658 100644 --- a/src/lotus-plasma-theme.cpp +++ b/src/lotus-plasma-theme.cpp @@ -4,6 +4,7 @@ */ #include "lotus-plasma-theme.h" +#include "lotus-utils.h" #include #include @@ -31,11 +32,6 @@ namespace fcitx { return dir + "/" + name; } - static bool isDirectory(const std::string& path) { - struct stat st{}; - return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode); - } - static std::vector splitPathList(const std::string& list) { std::vector result; std::stringstream ss(list); @@ -47,9 +43,9 @@ namespace fcitx { return result; } - static std::string envOr(const char* name, const std::string& fallback) { - const char* value = std::getenv(name); - return (value != nullptr && *value != '\0') ? std::string(value) : fallback; + static std::string envOr(const std::string& name, const std::string& fallback) { + std::string value = getEnv(name); + return !value.empty() ? value : fallback; } // Reads "key=value" from "[group]" of a KConfig-style INI file. @@ -112,7 +108,7 @@ namespace fcitx { if (component > 255) return std::nullopt; if (count < rgb.size()) - rgb[count] = component; + rgb[count] = component; // NOLINT ++count; } if (count != 3 && count != 4) @@ -127,12 +123,9 @@ namespace fcitx { const auto rgb = parseRgb(*value); if (!rgb) return std::nullopt; - const int gray = ((*rgb)[0] * 11 + (*rgb)[1] * 16 + (*rgb)[2] * 5) / 32; - return gray < 192; - } - static std::optional isDarkColorsFile(const std::string& path) { - return isDarkBackground(readIniValue(path, "Colors:Window", "BackgroundNormal")); + const int gray = (((*rgb)[0] * 11) + ((*rgb)[1] * 16) + ((*rgb)[2] * 5)) / 32; + return gray < 192; } static std::optional isDarkSystemScheme(const PlasmaThemeSearchPaths& paths) { @@ -146,7 +139,7 @@ namespace fcitx { for (const auto& dir : dataSearchDirs(paths)) { const std::string file = joinPath(dir, "color-schemes/" + *scheme + ".colors"); if (access(file.c_str(), R_OK) == 0) - return isDarkColorsFile(file); + return isDarkBackground(readIniValue(file, "Colors:Window", "BackgroundNormal")); } return std::nullopt; } @@ -165,12 +158,11 @@ namespace fcitx { const std::string kdeDefaults = joinPath(paths.configHome, "kdedefaults"); if (std::find(paths.configDirs.begin(), paths.configDirs.end(), kdeDefaults) == paths.configDirs.end()) paths.configDirs.insert(paths.configDirs.begin(), kdeDefaults); + return paths; } - bool isKdePlasmaSession(const char* xdgCurrentDesktop) { - if (xdgCurrentDesktop == nullptr) - return false; + bool isKdePlasmaSession(const std::string& xdgCurrentDesktop) { const auto desktops = splitPathList(xdgCurrentDesktop); return std::find(desktops.begin(), desktops.end(), "KDE") != desktops.end(); } @@ -185,11 +177,12 @@ namespace fcitx { // "default") follows the system colour scheme. for (const auto& dir : dataSearchDirs(paths)) { const std::string themeDir = joinPath(dir, "plasma/desktoptheme/" + *theme); - if (!isDirectory(themeDir)) + struct stat st{}; + if (stat(themeDir.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue; const std::string colors = joinPath(themeDir, "colors"); if (access(colors.c_str(), R_OK) == 0) - return isDarkColorsFile(colors); + return isDarkBackground(readIniValue(colors, "Colors:Window", "BackgroundNormal")); break; } return isDarkSystemScheme(paths); diff --git a/src/lotus-plasma-theme.h b/src/lotus-plasma-theme.h index ba95a2e7..20ac7d57 100644 --- a/src/lotus-plasma-theme.h +++ b/src/lotus-plasma-theme.h @@ -47,7 +47,7 @@ namespace fcitx { /** * @brief Tells whether XDG_CURRENT_DESKTOP names KDE. */ - bool isKdePlasmaSession(const char* xdgCurrentDesktop); + bool isKdePlasmaSession(const std::string& xdgCurrentDesktop); /** * @brief Reports whether the Plasma panel background is dark. diff --git a/src/lotus-utils.cpp b/src/lotus-utils.cpp index d3638ebd..9ecc5f21 100644 --- a/src/lotus-utils.cpp +++ b/src/lotus-utils.cpp @@ -116,3 +116,8 @@ void eraseLastUtf8Codepoint(std::string& buffer) { } buffer.erase(pos); } + +std::string getEnv(const std::string& name) { + const char* value = std::getenv(name.c_str()); + return ((value != nullptr) && ((*value) != 0)) ? value : ""; +} \ No newline at end of file diff --git a/src/lotus-utils.h b/src/lotus-utils.h index fc95b0d4..71b7463c 100644 --- a/src/lotus-utils.h +++ b/src/lotus-utils.h @@ -116,4 +116,11 @@ struct KeyEntry { uint32_t state; ///< Key state (modifiers) }; +/** + * @brief get environement variable. + * @param name Name of the variable. + * @return Value of the variable. + */ +std::string getEnv(const std::string& name); + #endif // _FCITX5_LOTUS_UTILS_H_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f63b3291..f74f2ace 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -80,17 +80,4 @@ add_test(NAME emoji_backspace_utf8 COMMAND emoji_backspace_utf8) set_tests_properties(emoji_backspace_utf8 PROPERTIES LABELS "unit;emoji" TIMEOUT 20 -) - -add_executable(plasma_panel_theme - plasma-panel-theme.cpp - ${PROJECT_SOURCE_DIR}/src/lotus-plasma-theme.cpp -) -target_include_directories(plasma_panel_theme PRIVATE - ${PROJECT_SOURCE_DIR}/src -) -add_test(NAME plasma_panel_theme COMMAND plasma_panel_theme) -set_tests_properties(plasma_panel_theme PROPERTIES - LABELS "unit;icon" - TIMEOUT 20 -) +) \ No newline at end of file diff --git a/test/emoji-history-page-navigation.cpp b/test/emoji-history-page-navigation.cpp index 4ead217e..4c7ad08c 100644 --- a/test/emoji-history-page-navigation.cpp +++ b/test/emoji-history-page-navigation.cpp @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "lotus-engine.h" +#include "lotus-utils.h" #include "test-input-context.h" #include #include -#include #include #include #include @@ -35,7 +35,7 @@ namespace { int main() { configureTestPaths("fcitx5-lotus-emoji-history-page-navigation"); - const auto historyPath = std::filesystem::path(std::getenv("XDG_CONFIG_HOME")) / "fcitx5/conf/lotus-emoji-history.conf"; + const auto historyPath = std::filesystem::path(getEnv("XDG_CONFIG_HOME")) / "fcitx5/conf/lotus-emoji-history.conf"; std::ofstream history(historyPath); const char* emoji[] = {"😀", "😁", "😂", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍", "😘", "😗", "😙", "😚", "🙂", "🤗"}; for (int i = 0; i < 18; ++i) diff --git a/test/plasma-panel-theme.cpp b/test/plasma-panel-theme.cpp deleted file mode 100644 index 43942196..00000000 --- a/test/plasma-panel-theme.cpp +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// -// Unit regression test for isPlasmaPanelDark() (issue #374). -// -// Each case builds a throwaway XDG tree (config + data directories) that -// mirrors a real KDE setup, then asks whether the panel is dark. The first -// case is the reported bug: the default Fedora 44 look ships a light -// application colour scheme and the breeze-dark Plasma Style, stored in -// ~/.config/kdedefaults, so the tray icon must be the white one. -// -#include "lotus-plasma-theme.h" - -#include -#include -#include -#include -#include -#include - -#include - -namespace { - - namespace fs = std::filesystem; - - const char* const kDark = "32,35,38"; // Breeze Dark window background - const char* const kLight = "239,240,241"; // Breeze Light window background - - int failures = 0; - - void writeFile(const fs::path& path, const std::string& content) { - fs::create_directories(path.parent_path()); - std::ofstream(path) << content; - } - - std::string colorsFile(const char* background) { - return std::string("[Colors:Window]\nBackgroundAlternate=1,2,3\nBackgroundNormal=") + background + "\nForegroundNormal=9,9,9\n"; - } - - std::string show(std::optional value) { - if (!value) - return "nullopt"; - return *value ? "dark" : "light"; - } - - void check(const char* name, bool ok, const std::string& detail) { - std::printf("%s %s%s%s\n", ok ? "PASS" : "FAIL", name, ok ? "" : " — ", ok ? "" : detail.c_str()); - if (!ok) - ++failures; - } - - // One isolated XDG tree per case. - struct Tree { - fs::path root; - fcitx::PlasmaThemeSearchPaths paths; - - explicit Tree(const char* name) : root(fs::temp_directory_path() / ("lotus-plasma-theme-test-" + std::to_string(getpid()) + "-" + name)) { - fs::remove_all(root); - paths.configHome = (root / "config").string(); - paths.configDirs = {(root / "config/kdedefaults").string(), (root / "etc/xdg").string()}; - paths.dataHome = (root / "local/share").string(); - paths.dataDirs = {(root / "usr/share").string()}; - // Themes installed on every KDE system. - writeFile(root / "usr/share/plasma/desktoptheme/default/metadata.json", "{}"); - writeFile(root / "usr/share/plasma/desktoptheme/breeze-dark/colors", colorsFile(kDark)); - writeFile(root / "usr/share/plasma/desktoptheme/breeze-light/colors", colorsFile(kLight)); - } - ~Tree() { - fs::remove_all(root); - } - void write(const char* relative, const std::string& content) { - writeFile(root / relative, content); - } - }; - - void expect(const char* name, Tree& tree, std::optional expected) { - const auto actual = fcitx::isPlasmaPanelDark(tree.paths); - check(name, actual == expected, "expected " + show(expected) + ", got " + show(actual)); - } - -} // namespace - -int main() { - { - // The #374 report: light apps, dark panel, set by the Global Theme. - Tree t("fedora-default"); - t.write("config/kdedefaults/plasmarc", "[Theme]\nname=breeze-dark\n"); - t.write("config/kdeglobals", colorsFile(kLight)); - expect("P1 fedora_default_light_apps_dark_panel", t, true); - } - { - Tree t("breeze-light-scheme"); - t.write("config/plasmarc", "[Theme]\nname=default\n"); - t.write("config/kdeglobals", colorsFile(kLight)); - expect("P2 breeze_default_follows_light_scheme", t, false); - } - { - Tree t("breeze-dark-scheme"); - t.write("config/plasmarc", "[Theme]\nname=default\n"); - t.write("config/kdeglobals", colorsFile(kDark)); - expect("P3 breeze_default_follows_dark_scheme", t, true); - } - { - Tree t("light-panel-dark-apps"); - t.write("config/plasmarc", "[Theme]\nname=breeze-light\n"); - t.write("config/kdeglobals", colorsFile(kDark)); - expect("P4 light_panel_with_dark_apps", t, false); - } - { - Tree t("user-overrides-defaults"); - t.write("config/kdedefaults/plasmarc", "[Theme]\nname=breeze-dark\n"); - t.write("config/plasmarc", "[Theme]\nname=breeze-light\n"); - t.write("config/kdeglobals", colorsFile(kDark)); - expect("P5 user_plasmarc_overrides_global_theme", t, false); - } - { - Tree t("user-theme-shadows-system"); - t.write("config/plasmarc", "[Theme]\nname=breeze-light\n"); - t.write("local/share/plasma/desktoptheme/breeze-light/colors", colorsFile(kDark)); - t.write("config/kdeglobals", colorsFile(kLight)); - expect("P6 user_installed_theme_shadows_system", t, true); - } - { - // A user copy without a colors file follows the scheme, even though - // the system copy of the same name has dark colours. - Tree t("user-theme-no-colors"); - t.write("config/plasmarc", "[Theme]\nname=breeze-dark\n"); - t.write("local/share/plasma/desktoptheme/breeze-dark/metadata.json", "{}"); - t.write("config/kdeglobals", colorsFile(kLight)); - expect("P7 user_theme_without_colors_follows_scheme", t, false); - } - { - Tree t("missing-theme"); - t.write("config/plasmarc", "[Theme]\nname=not-installed\n"); - t.write("config/kdeglobals", colorsFile(kDark)); - expect("P8 missing_theme_follows_scheme", t, true); - } - { - Tree t("scheme-name-only"); - t.write("config/kdedefaults/kdeglobals", "[General]\nColorScheme=BreezeDark\n"); - t.write("usr/share/color-schemes/BreezeDark.colors", colorsFile(kDark)); - expect("P9 scheme_name_only_resolves_colors_file", t, true); - } - { - Tree t("nothing-known"); - expect("P10 nothing_known", t, std::nullopt); - } - - check("S1 kde_session", fcitx::isKdePlasmaSession("KDE"), "KDE not recognised"); - check("S2 gnome_session", !fcitx::isKdePlasmaSession("GNOME"), "GNOME taken for KDE"); - check("S3 no_desktop", !fcitx::isKdePlasmaSession(nullptr), "nullptr taken for KDE"); - - { - setenv("HOME", "/h", 1); - setenv("XDG_CONFIG_HOME", "/c", 1); - setenv("XDG_CONFIG_DIRS", "/etc/xdg", 1); - const auto p = fcitx::plasmaThemeSearchPathsFromEnv(); - const bool ok = p.configDirs.size() == 2 && p.configDirs[0] == "/c/kdedefaults" && p.configDirs[1] == "/etc/xdg"; - check("E1 env_adds_kdedefaults", ok, "configDirs lacks /c/kdedefaults in front of /etc/xdg"); - } - { - setenv("HOME", "/h", 1); - unsetenv("XDG_CONFIG_HOME"); - unsetenv("XDG_CONFIG_DIRS"); - unsetenv("XDG_DATA_HOME"); - unsetenv("XDG_DATA_DIRS"); - const auto p = fcitx::plasmaThemeSearchPathsFromEnv(); - const bool ok = - p.configHome == "/h/.config" && p.dataHome == "/h/.local/share" && p.dataDirs.size() == 2 && p.dataDirs[0] == "/usr/local/share" && p.dataDirs[1] == "/usr/share"; - check("E2 env_xdg_defaults", ok, "XDG defaults not applied"); - } - - std::printf("%d failure(s)\n", failures); - return failures == 0 ? 0 : 1; -} From f6298d62c685fbbd5cb5be1c2e911a396cfa0bd8 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Tue, 15 Sep 2026 23:24:45 +0700 Subject: [PATCH 3/3] add file to cmake again --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 92916579..c23d4530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ set(fcitx_lotus_core_sources ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-monitor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-icon-resolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/lotus-plasma-theme.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/emoji.cpp )