Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
34 changes: 22 additions & 12 deletions src/lotus-engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "lotus-utils.h"
#include "lotus-icon-resolver.h"
#include "ack-apps.h"
#include "lotus-plasma-theme.h"
#include <optional>
#include <sys/socket.h>
#include <utility>
Expand Down Expand Up @@ -108,12 +109,21 @@ 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(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")) {
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;
}
Expand Down Expand Up @@ -148,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)) {
Expand Down Expand Up @@ -198,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();
{
Expand Down Expand Up @@ -1198,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) {
Expand Down
191 changes: 191 additions & 0 deletions src/lotus-plasma-theme.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/

#include "lotus-plasma-theme.h"
#include "lotus-utils.h"

#include <algorithm>
#include <array>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <sys/stat.h>
#include <unistd.h>

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 std::vector<std::string> splitPathList(const std::string& list) {
std::vector<std::string> 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 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.
static std::optional<std::string> 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<std::string> 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<std::string> dataSearchDirs(const PlasmaThemeSearchPaths& paths) {
std::vector<std::string> 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<std::array<int, 3>> parseRgb(const std::string& value) {
std::array<int, 3> 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; // NOLINT
++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<bool> isDarkBackground(const std::optional<std::string>& 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<bool> 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 isDarkBackground(readIniValue(file, "Colors:Window", "BackgroundNormal"));
}
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 std::string& xdgCurrentDesktop) {
const auto desktops = splitPathList(xdgCurrentDesktop);
return std::find(desktops.begin(), desktops.end(), "KDE") != desktops.end();
}

std::optional<bool> 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);
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 isDarkBackground(readIniValue(colors, "Colors:Window", "BackgroundNormal"));
break;
}
return isDarkSystemScheme(paths);
}

} // namespace fcitx
65 changes: 65 additions & 0 deletions src/lotus-plasma-theme.h
Original file line number Diff line number Diff line change
@@ -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 <optional>
#include <string>
#include <vector>

namespace fcitx {

struct PlasmaThemeSearchPaths {
// Config directories searched in KConfig cascade order: the user
// directory first, then each system directory.
std::string configHome;
std::vector<std::string> configDirs;
// Data directories holding plasma/desktoptheme and color-schemes.
std::string dataHome;
std::vector<std::string> dataDirs;
};

/**
* @brief Builds search paths from the XDG environment variables.
*
* Adds "<configHome>/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 std::string& 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<bool> isPlasmaPanelDark(const PlasmaThemeSearchPaths& paths);

} // namespace fcitx

#endif // _FCITX5_LOTUS_PLASMA_THEME_H_
5 changes: 5 additions & 0 deletions src/lotus-utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 : "";
}
7 changes: 7 additions & 0 deletions src/lotus-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_
2 changes: 1 addition & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ add_test(NAME emoji_backspace_utf8 COMMAND emoji_backspace_utf8)
set_tests_properties(emoji_backspace_utf8 PROPERTIES
LABELS "unit;emoji"
TIMEOUT 20
)
)
4 changes: 2 additions & 2 deletions test/emoji-history-page-navigation.cpp
Original file line number Diff line number Diff line change
@@ -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 <fcitx/candidatelist.h>
#include <fcitx/inputpanel.h>

#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
Expand Down Expand Up @@ -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)
Expand Down
Loading