From a6d86fea1af783c7d876717b139c5574b8433dc6 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Thu, 10 Sep 2026 18:35:20 +0700 Subject: [PATCH 01/25] Update bamboo-core --- bamboo/bamboo-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bamboo/bamboo-core b/bamboo/bamboo-core index f976d2d1..30e84575 160000 --- a/bamboo/bamboo-core +++ b/bamboo/bamboo-core @@ -1 +1 @@ -Subproject commit f976d2d1f639fca86f798a8f978b6883c854d45b +Subproject commit 30e8457584ff36767e9de40161728c004b1281b0 From 46039ee27fe02988565b80bd0754e3c2e5d18d21 Mon Sep 17 00:00:00 2001 From: Coolllyn Date: Fri, 11 Sep 2026 07:33:13 +0700 Subject: [PATCH 02/25] update debian/copyright --- packaging/debian/copyright | 52 +++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/packaging/debian/copyright b/packaging/debian/copyright index eb76b080..e6f88ec0 100644 --- a/packaging/debian/copyright +++ b/packaging/debian/copyright @@ -4,13 +4,30 @@ Upstream-Contact: Nguyen Hoang Ky Source: https://github.com/LotusInputMethod/fcitx5-lotus Files: * -Copyright: 2026 Nguyen Hoang Ky +Copyright: 2022 CSSlayer + 2025 Vo Ngo Hoan Thanh + 2026 Nguyen Hoang Ky + 2026 fcitx5-lotus contributors License: GPL-3.0-or-later Files: debian/* Copyright: 2026 Nguyen Hoang Ky + 2026 fcitx5-lotus contributors License: GPL-3.0-or-later +Files: bamboo/* +Copyright: 2022 CSSlayer + 2026 Nguyen Hoang Ky + 2026 fcitx5-lotus contributors +License: LGPL-2.1-or-later + +Files: bamboo/bamboo-core/* +Copyright: 2018 Luong Thanh Lam + 2018 bamboo-core original contributors + 2026 Nguyen Hoang Ky + 2026 fcitx5-lotus contributors +License: MIT + License: GPL-3.0-or-later This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,3 +41,36 @@ License: GPL-3.0-or-later . On Debian systems, the complete text of the GNU General Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". + +License: LGPL-2.1-or-later + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + . + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + . + On Debian systems, the complete text of the GNU Lesser General Public + License version 2.1 can be found in "/usr/share/common-licenses/LGPL-2.1". + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. From 270f79811f250450677a4b39b5140d06b30a314b Mon Sep 17 00:00:00 2001 From: Nguyen Hong Hiep Date: Fri, 11 Sep 2026 18:30:46 +0700 Subject: [PATCH 03/25] nix: update nix package file --- nix/packages/fcitx5-lotus/default.nix | 43 +++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/nix/packages/fcitx5-lotus/default.nix b/nix/packages/fcitx5-lotus/default.nix index 85cde5fa..a10f2323 100644 --- a/nix/packages/fcitx5-lotus/default.nix +++ b/nix/packages/fcitx5-lotus/default.nix @@ -13,6 +13,7 @@ libinput, librsvg, libx11, + nix-update-script, pkg-config, python3, qt6, @@ -35,11 +36,25 @@ stdenv.mkDerivation (finalAttrs: { src = fetchFromGitHub { owner = "LotusInputMethod"; repo = "fcitx5-lotus"; - tag = "v${finalAttrs.version}"; + rev = "v${finalAttrs.version}"; hash = "sha256-kOIs8nLSF93xDIU7v8Wldyw+Zs5NEMJqZA/42TN4oYM="; fetchSubmodules = true; }; + vendorDir = finalAttrs.passthru."go-modules"; + + passthru = { + "go-modules" = + (buildGoModule { + pname = "fcitx5-lotus-go-modules"; + inherit (finalAttrs) version src; + modRoot = "bamboo"; + vendorHash = "sha256-CNDYjxDfqh9nGs5vlpb/7qXZeNtkvegC5nPvBOZcDrc="; + }).goModules; + + updateScript = nix-update-script { }; + }; + nativeBuildInputs = [ cmake gettext @@ -63,13 +78,11 @@ stdenv.mkDerivation (finalAttrs: { udev ]; - vendorDir = - (buildGoModule { - pname = "fcitx5-lotus-go-modules"; - inherit (finalAttrs) version src; - modRoot = "bamboo"; - vendorHash = "sha256-CNDYjxDfqh9nGs5vlpb/7qXZeNtkvegC5nPvBOZcDrc="; - }).goModules; + strictDeps = true; + + __structuredAttrs = true; + + dontWrapQtApps = true; preConfigure = '' export GOCACHE=$TMPDIR/go-cache @@ -113,10 +126,16 @@ stdenv.mkDerivation (finalAttrs: { --prefix XDG_DATA_DIRS : "${hicolor-icon-theme}/share" ''; - meta = with lib; { - description = "Fcitx5 Lotus input method for Vietnamese typing"; + meta = { + description = "Vietnamese input method engine for Fcitx5"; homepage = "https://github.com/LotusInputMethod/fcitx5-lotus"; - license = licenses.gpl3; - platforms = platforms.linux; + license = with lib.licenses; [ + gpl3Plus + lgpl21Plus + ]; + maintainers = with lib.maintainers; [ + justanoobcoder + ]; + platforms = lib.platforms.linux; }; }) From 5767afa53d7ba1a4f60fbd49952b818d24e87a4c Mon Sep 17 00:00:00 2001 From: Nguyen Hong Hiep Date: Fri, 11 Sep 2026 18:31:02 +0700 Subject: [PATCH 04/25] ci: add update nix hash workflow --- .github/workflows/update-nix-hash.yaml | 89 ++++++++++++++++++++++++++ flake.lock | 12 ++-- nix/packages/fcitx5-lotus/default.nix | 2 +- 3 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/update-nix-hash.yaml diff --git a/.github/workflows/update-nix-hash.yaml b/.github/workflows/update-nix-hash.yaml new file mode 100644 index 00000000..028646a0 --- /dev/null +++ b/.github/workflows/update-nix-hash.yaml @@ -0,0 +1,89 @@ +name: Update Nix Hash on Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Update version:' + required: false + +permissions: + contents: write + pull-requests: write + +concurrency: + group: update-nix-hash + cancel-in-progress: false + +jobs: + update-hash: + name: Update package hash + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: Determine version + id: version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Run nix-update via shim + run: | + cat > update-shim.nix << 'EOF' + { pkgs ? import { } }: + { + fcitx5-lotus = pkgs.callPackage ./nix/packages/fcitx5-lotus { }; + } + EOF + + nix run nixpkgs#nix-update -- \ + --file ./update-shim.nix \ + fcitx5-lotus \ + --version="${{ steps.version.outputs.version }}" + + rm update-shim.nix + + - name: Build to verify + run: nix build -L --no-link .#packages.x86_64-linux.fcitx5-lotus + + - name: Check for changes + id: git-check + run: | + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create Pull Request + if: steps.git-check.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + base: main + commit-message: "nix: update fcitx5-lotus to ${{ steps.version.outputs.version }}" + title: "nix: update to v${{ steps.version.outputs.version }}" + body: | + Tự động cập nhật version và hash cho Nix sau khi release `v${{ steps.version.outputs.version }}`. + Đã build thành công trước khi tạo PR này. + branch: auto-update-nix-${{ steps.version.outputs.version }} + delete-branch: true diff --git a/flake.lock b/flake.lock index ed90709b..3b76d78e 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778443072, - "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "lastModified": 1789006805, + "narHash": "sha256-xB8mKMOx1IA9vTDNLmJZ6n4wCMq/cuWBBOzGCRnqxrU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "rev": "8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe", "type": "github" }, "original": { @@ -84,11 +84,11 @@ ] }, "locked": { - "lastModified": 1765361626, - "narHash": "sha256-kX0Dp/kYSRbQ+yd9e3lmmUWdNbipufvKfL2IzbrSpnY=", + "lastModified": 1784328457, + "narHash": "sha256-4R5WV74NHhPk21ctoPmjL7nQ6txDnoBLwSWULvIw0TY=", "owner": "snowfallorg", "repo": "lib", - "rev": "c566ad8b7352c30ec3763435de7c8f1c46ebb357", + "rev": "6ee3542cb459ca4b038cfe50ceb8797f05cdabad", "type": "github" }, "original": { diff --git a/nix/packages/fcitx5-lotus/default.nix b/nix/packages/fcitx5-lotus/default.nix index a10f2323..6251bcd3 100644 --- a/nix/packages/fcitx5-lotus/default.nix +++ b/nix/packages/fcitx5-lotus/default.nix @@ -36,7 +36,7 @@ stdenv.mkDerivation (finalAttrs: { src = fetchFromGitHub { owner = "LotusInputMethod"; repo = "fcitx5-lotus"; - rev = "v${finalAttrs.version}"; + tag = "v${finalAttrs.version}"; hash = "sha256-kOIs8nLSF93xDIU7v8Wldyw+Zs5NEMJqZA/42TN4oYM="; fetchSubmodules = true; }; From 6dc09ec08307fe1e8d784ceffc84a272f38f3218 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Sat, 12 Sep 2026 00:16:05 +0700 Subject: [PATCH 05/25] feat(engine): notify when app mode changes via the mode menu --- src/lotus-engine.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index b30222c0..4b4d79e6 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -632,6 +632,8 @@ namespace fcitx { setMode(selectedMode.value(), ic); if (selectedMode == LotusMode::Emoji) { state->updateEmojiPreedit(); + } else { + showCycleModeNotification(selectedMode.value(), ic); } } } @@ -966,6 +968,8 @@ namespace fcitx { if (mode == LotusMode::Emoji) { auto* state = ic->propertyFor(&factory_); state->updateEmojiPreedit(); + } else { + showCycleModeNotification(mode, ic); } }; }; From 939f01a74cab272581df64c7059c5414252feb8b Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Sat, 12 Sep 2026 00:34:07 +0700 Subject: [PATCH 06/25] fix(engine): keep focused app's per-app mode rule across config reload and new input contexts --- src/lotus-engine.cpp | 7 ++- src/lotus-state.cpp | 1 - test/CMakeLists.txt | 1 + test/app-rule-reset.cpp | 109 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 test/app-rule-reset.cpp diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index 4b4d79e6..2442ff77 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -264,6 +264,7 @@ namespace fcitx { std::filesystem::create_directories(configDir); } reloadConfig(); + realMode = config_.mode.value(); instance_->inputContextManager().registerProperty("LotusState", &factory_); appRulesPath_ = configDir + "/lotus-app-rules.conf"; loadAppRules(); @@ -785,8 +786,12 @@ namespace fcitx { instance_->inputContextManager().foreach ([this](InputContext* ic) { auto* state = ic->propertyFor(&factory_); state->setEngine(); - if (ic->hasFocus()) + if (ic->hasFocus()) { + // Re-resolve the focused window's rule; setEngine() must not + // reset it to the global mode. + setMode(getAppRule(getProgramName(ic)), ic); state->reset(); + } return true; }); } diff --git a/src/lotus-state.cpp b/src/lotus-state.cpp index 600424c0..a62df55e 100644 --- a/src/lotus-state.cpp +++ b/src/lotus-state.cpp @@ -41,7 +41,6 @@ namespace fcitx { void LotusState::setEngine() { lotusEngine_.reset(); - realMode = engine_->config().mode.value(); if (engine_->config().inputMethod.value() == "Custom") { const auto& keymaps = *engine_->customKeymap().customKeymap; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 518cd186..fbb2340c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -56,6 +56,7 @@ function(add_bamboo_test target source labels) endfunction() add_lotus_headless_test(preedit_lifecycle preedit-lifecycle.cpp "integration;preedit") +add_lotus_headless_test(app_rule_reset app-rule-reset.cpp "integration;apprule") add_lotus_headless_test(preedit_smoke_client preedit-smoke-client.cpp "integration;preedit") add_lotus_headless_test(preedit_smoke_server preedit-smoke-server.cpp "integration;preedit") add_lotus_headless_test(smooth_buffered_key_replay diff --git a/test/app-rule-reset.cpp b/test/app-rule-reset.cpp new file mode 100644 index 00000000..2c547221 --- /dev/null +++ b/test/app-rule-reset.cpp @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/** + * @file app-rule-reset.cpp + * @brief Headless regression test: a per-app mode rule must survive a config + * reload / new input context for the focused window. + * + * Bug: LotusState::setEngine() wrote the global `realMode` from the global Mode + * option without resolving the per-app rule. setEngine() runs on every new + * input context and for every input context on config reload (refreshEngine), + * so a focused app with its own rule (e.g. Off) silently dropped back to the + * global mode (e.g. Preedit) until the next focus change. + * + * The test focuses a context whose program ("test") has an Off rule while the + * global mode is Preedit, then reloads the config without changing focus and + * asserts the resolved mode is still the rule (Off). + */ + +#include "lotus-engine.h" +#include "lotus-utils.h" +#include "test-input-context.h" + +#include +#include +#include +#include +#include + +namespace { + + void reportFailure(const std::string& step, const std::string& expected, const std::string& actual, const std::string& meaning) { + std::cerr << "Step: " << step << '\n'; + std::cerr << "Expected: " << expected << '\n'; + std::cerr << "Actual: " << actual << '\n'; + std::cerr << "Meaning: " << meaning << '\n'; + } + + std::string modeName(fcitx::LotusMode mode) { + switch (mode) { + case fcitx::LotusMode::Off: return "Off"; + case fcitx::LotusMode::Preedit: return "Preedit"; + case fcitx::LotusMode::Smooth: return "Smooth"; + case fcitx::LotusMode::Uinput: return "Uinput"; + case fcitx::LotusMode::SuperSmooth: return "SuperSmooth"; + case fcitx::LotusMode::SurroundingText: return "SurroundingText"; + case fcitx::LotusMode::Emoji: return "Emoji"; + case fcitx::LotusMode::Minecraft: return "Minecraft"; + default: return "Unknown"; + } + } + +} // namespace + +int main() { + const char* testName = "fcitx5-lotus-app-rule-reset"; + configureTestPaths(testName); + + // Per-app rule for the mock context's program name ("test"): Off. + // Must be on disk before the engine is constructed (loadAppRules runs in + // the constructor). + const auto rulesFile = std::filesystem::temp_directory_path() / testName / "config/fcitx5/conf/lotus-app-rules.conf"; + { + std::ofstream file(rulesFile, std::ios::trunc); + if (!file.is_open()) { + reportFailure("write app rules file", "file open", rulesFile.string(), "the test needs the app rule on disk"); + return 1; + } + file << "test=0\n"; + } + + TestInstance testInstance; + fcitx::LotusEngine engine(&testInstance.instance); + + // Global mode Preedit, differing from the app's Off rule. + fcitx::RawConfig config; + config.setValueByPath("Mode", "Preedit"); + config.setValueByPath("InputMethod", "Telex"); + engine.setConfig(config); + if (engine.config().mode.value() != fcitx::LotusMode::Preedit) { + reportFailure("configure global Preedit", "mode=Preedit", "global mode differs", "the test needs the global mode to differ from the app rule"); + return 1; + } + + auto context = std::make_unique(&testInstance.instance); + context->focusIn(); + fcitx::InputMethodEntry entry("lotus", "Lotus", "vi", "lotus"); + fcitx::InputContextEvent focus(context.get(), fcitx::EventType::InputContextFocusIn); + engine.activate(entry, focus); + + if (::realMode.load() != fcitx::LotusMode::Off) { + reportFailure("activate with rule Off", "realMode=Off", "realMode=" + modeName(::realMode.load()), "activate() must resolve the per-app rule for the focused window"); + return 1; + } + + // Simulate a config reload / a new input context appearing without any + // focus change: setEngine() runs for every context but must not clobber the + // focused window's resolved rule. + fcitx::RawConfig reloaded; + reloaded.setValueByPath("Mode", "Preedit"); + reloaded.setValueByPath("InputMethod", "Telex"); + engine.setConfig(reloaded); + + if (::realMode.load() != fcitx::LotusMode::Off) { + reportFailure("config reload keeps focused app rule", "realMode=Off", "realMode=" + modeName(::realMode.load()), + "a config reload must not reset the focused window from its per-app rule back to the global mode"); + return 1; + } + + return 0; +} From 486574284aa767fb5afae1e84883eed66ef9a4cf Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Sat, 12 Sep 2026 01:03:03 +0700 Subject: [PATCH 07/25] fix(engine): survive Chromium X11 focus bounce in uinput modes and add isUinputMode helper --- src/lotus-engine.cpp | 33 ++++++++++++++++++++++++++++----- src/lotus-state.h | 1 + src/lotus-utils.cpp | 4 ++++ src/lotus-utils.h | 7 +++++++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index 2442ff77..72857bf1 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -441,9 +441,24 @@ namespace fcitx { updateCharsetAction(event.inputContext()); - setMode(targetMode, event.inputContext()); + auto* state = ic->propertyFor(&factory_); - auto* state = ic->propertyFor(&factory_); + const bool uinputMode = isUinputMode(targetMode); + const bool focusBounce = uinputMode && state->lastDeactivateTime_ > 0 && now_ms() - state->lastDeactivateTime_ < 100; + const bool resumeReplacement = focusBounce && state->deletionInterruptedAt_ > 0 && is_deleting_.load(); + state->deletionInterruptedAt_ = 0; + + if (!resumeReplacement) { + is_deleting_.store(false); + } + + if (focusBounce) { + realMode = targetMode; + ic->updateUserInterface(UserInterfaceComponent::StatusArea); + LOTUS_INFO("Focus bounce: keep word buffers"); + } else { + setMode(targetMode, event.inputContext()); + } // Workaround for chromium wayland issue where suggestions cause a doubled // first character. Forwarding may prevent BS from being sent @@ -457,7 +472,7 @@ namespace fcitx { state->waitAck_ = false; if (*config_.fixUinputWithAck) { - if (targetMode == LotusMode::Uinput || targetMode == LotusMode::Smooth || targetMode == LotusMode::Minecraft || targetMode == LotusMode::SuperSmooth) { + if (isUinputMode(targetMode)) { #if __cplusplus >= 202002L std::ranges::transform(appName, appName.begin(), ::tolower); #else @@ -480,7 +495,9 @@ namespace fcitx { } else if (surrvalid && !state->oldPreBuffer_.empty() && (now_ms() - state->lastDeactivateTime_) >= 100) { state->clearAllBuffers(); } - is_deleting_.store(false); + if (!resumeReplacement) { + is_deleting_.store(false); + } needEngineReset.store(false); if (targetMode == LotusMode::Emoji) { state->updateEmojiPreedit(); @@ -771,7 +788,13 @@ namespace fcitx { if (surrvalid && !state->oldPreBuffer_.empty()) state->clearAllBuffers(); } - is_deleting_.store(false); + const bool uinputMode = isUinputMode(realMode); + if (uinputMode && is_deleting_.load() && state->expected_backspaces_ > 0) { + state->deletionInterruptedAt_ = now_ms(); + LOTUS_INFO("Replacement interrupted by focus out"); + } else { + is_deleting_.store(false); + } needEngineReset.store(false); ic->inputPanel().reset(); ic->updateUserInterface(UserInterfaceComponent::InputPanel); diff --git a/src/lotus-state.h b/src/lotus-state.h index 1f1df591..f72645b6 100644 --- a/src/lotus-state.h +++ b/src/lotus-state.h @@ -104,6 +104,7 @@ namespace fcitx { bool shouldCapitalize_ = false; bool isPrevPunctuation_ = false; int64_t lastDeactivateTime_ = 0; + int64_t deletionInterruptedAt_ = 0; ///< when deactivate() cut an in-flight replacement (0 = none) bool wa_chromium_flag = false; bool tracking_modifier_tap_ = false; ///< Selected modifier held, waiting for consecutive keyup bool macro_skip_ = false; ///< Macro disabled for the current word diff --git a/src/lotus-utils.cpp b/src/lotus-utils.cpp index 349b40b9..d3638ebd 100644 --- a/src/lotus-utils.cpp +++ b/src/lotus-utils.cpp @@ -62,6 +62,10 @@ bool isBackspace(uint32_t sym) { return sym == 65288 || sym == 8 || sym == FcitxKey_BackSpace; } +bool isUinputMode(fcitx::LotusMode mode) { + return mode == fcitx::LotusMode::Uinput || mode == fcitx::LotusMode::Smooth || mode == fcitx::LotusMode::SuperSmooth || mode == fcitx::LotusMode::Minecraft; +} + int compareAndSplitStrings(const std::string& A, const std::string& B, std::string& deletedPart, std::string& addedPart) { size_t i = 0; size_t j = 0; diff --git a/src/lotus-utils.h b/src/lotus-utils.h index 91793e72..fc95b0d4 100644 --- a/src/lotus-utils.h +++ b/src/lotus-utils.h @@ -66,6 +66,13 @@ int64_t now_ms(); */ bool isBackspace(uint32_t sym); +/** + * @brief Whether a mode delivers text through the uinput (fake backspace) path. + * @param mode Mode to check. + * @return True for Uinput, Smooth, SuperSmooth and Minecraft. + */ +bool isUinputMode(fcitx::LotusMode mode); + /** * @brief Erases the last UTF-8 codepoint from a string in place. * From 3d4595d1df78c818deb15cac512d4f8b8e23e105 Mon Sep 17 00:00:00 2001 From: Coolllyn Date: Fri, 11 Sep 2026 17:26:57 +0700 Subject: [PATCH 08/25] update debian/copyright --- packaging/debian/copyright | 112 ++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/packaging/debian/copyright b/packaging/debian/copyright index e6f88ec0..554bbc65 100644 --- a/packaging/debian/copyright +++ b/packaging/debian/copyright @@ -7,26 +7,103 @@ Files: * Copyright: 2022 CSSlayer 2025 Vo Ngo Hoan Thanh 2026 Nguyen Hoang Ky - 2026 fcitx5-lotus contributors License: GPL-3.0-or-later Files: debian/* Copyright: 2026 Nguyen Hoang Ky - 2026 fcitx5-lotus contributors License: GPL-3.0-or-later Files: bamboo/* Copyright: 2022 CSSlayer + 2018 Luong Thanh Lam 2026 Nguyen Hoang Ky - 2026 fcitx5-lotus contributors License: LGPL-2.1-or-later Files: bamboo/bamboo-core/* Copyright: 2018 Luong Thanh Lam - 2018 bamboo-core original contributors 2026 Nguyen Hoang Ky - 2026 fcitx5-lotus contributors -License: MIT +License: Expat + +Files: cmake/* +Copyright: 2016 Xuetian Weng + 2007 Alexander Neundorf + 2006 Chusslove Illich +License: BSD-3-Clause + +Files: data/dictionaries/* +Copyright: 1997-2003 The Free Vietnamese Dictionary Project +License: GPL-2.0-or-later + +Files: org.fcitx.Fcitx5.Addon.Lotus.metainfo.xml.in.in +Copyright: 2026 Nguyen Hoang Ky +License: CC0-1.0 + +License: BSD-3-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + . + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: CC0-1.0 + The person who associated a work with this deed has dedicated the work to + the public domain by waiving all of his or her rights to the work worldwide + under copyright law, including all related and neighboring rights, to the + extent allowed by law. + . + You can copy, modify, distribute and perform the work, even for commercial + purposes, all without asking permission. + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +License: GPL-2.0-or-later + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + On Debian systems, the complete text of the GNU General Public + License version 2 can be found in "/usr/share/common-licenses/GPL-2". License: GPL-3.0-or-later This program is free software; you can redistribute it and/or modify @@ -44,8 +121,8 @@ License: GPL-3.0-or-later License: LGPL-2.1-or-later This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation; either version 2.1 of the License, or + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. . This library is distributed in the hope that it will be useful, @@ -55,22 +132,3 @@ License: LGPL-2.1-or-later . On Debian systems, the complete text of the GNU Lesser General Public License version 2.1 can be found in "/usr/share/common-licenses/LGPL-2.1". - -License: MIT - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. From 5fcd8956343b227920fe92a09e97b259677ff876 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Sat, 12 Sep 2026 11:01:43 +0700 Subject: [PATCH 09/25] fix(bamboo): use an empty dictionary map instead of failing to create the engine when no dictionary is loaded --- bamboo/bamboo-c.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bamboo/bamboo-c.go b/bamboo/bamboo-c.go index aa3bd78a..7d560013 100644 --- a/bamboo/bamboo-c.go +++ b/bamboo/bamboo-c.go @@ -148,9 +148,9 @@ func EngineSetOption(engine uintptr, option *C.FcitxBambooEngineOption) { //export NewEngine func NewEngine(name *C.cchar, dictHandle uintptr, tableHandle uintptr) uintptr { - dict, ok := dictionaryFromHandle(dictHandle) - if !ok { - return 0 + dictionary := map[string]bool{} + if dict, ok := dictionaryFromHandle(dictHandle); ok { + dictionary = *dict } table, ok := macroTableFromHandle(tableHandle) @@ -163,7 +163,7 @@ func NewEngine(name *C.cchar, dictHandle uintptr, tableHandle uintptr) uintptr { var engine = &FcitxBambooEngine{ preeditor: bamboo.NewEngine(bamboo.ParseInputMethod(bamboo.InputMethodDefinitions, imName), bamboo.EstdFlags), macroTable: table, - dictionary: *dict, + dictionary: dictionary, autoNonVnRestore: true, ddFreeStyle: true, macroEnabled: false, @@ -182,9 +182,9 @@ func NewEngine(name *C.cchar, dictHandle uintptr, tableHandle uintptr) uintptr { //export NewCustomEngine func NewCustomEngine(definition **C.char, dictHandle uintptr, tableHandle uintptr) uintptr { - dict, ok := dictionaryFromHandle(dictHandle) - if !ok { - return 0 + dictionary := map[string]bool{} + if dict, ok := dictionaryFromHandle(dictHandle); ok { + dictionary = *dict } table, ok := macroTableFromHandle(tableHandle) @@ -206,7 +206,7 @@ func NewCustomEngine(definition **C.char, dictHandle uintptr, tableHandle uintpt var engine = &FcitxBambooEngine{ preeditor: bamboo.NewEngine(bamboo.ParseInputMethod(definitions, "Custom"), bamboo.EstdFlags), macroTable: table, - dictionary: *dict, + dictionary: dictionary, autoNonVnRestore: true, ddFreeStyle: true, macroEnabled: false, From d9a83010855582f5e5287a056e9ead6a1f908c8d Mon Sep 17 00:00:00 2001 From: nguyenphivn Date: Sat, 12 Sep 2026 15:37:23 +0700 Subject: [PATCH 10/25] fix(monitor): clamp the mouse-socket log length so the build is warning-free Building dev with GCC 16 emits one warning, and this is it: char_traits.h:432:56: warning: '__builtin_memcpy' accessing between 9223372036854775760 and 9223372036854775806 bytes ... [-Wrestrict] inlined from 'void mousePressResetThread()' at src/lotus-monitor.cpp:113 Not a real out-of-bounds read: n comes from recv() into a 16-byte buffer and the n <= 0 case has already returned, so n is in [1, sizeof(buf)] at that point. GCC cannot prove the upper bound, so it assumes the ssize_t could reach size_t range. Clamping the length makes the bound explicit and the build clean. Worth doing because it was the only warning left: one that always shows is one that trains you to skim past the next warning, which may be real. ctest 9/9. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TvNe693ge85eB4QPs6hpjV --- src/lotus-monitor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lotus-monitor.cpp b/src/lotus-monitor.cpp index 884f3a26..8d908d0a 100644 --- a/src/lotus-monitor.cpp +++ b/src/lotus-monitor.cpp @@ -8,6 +8,7 @@ #include "lotus-monitor.h" #include "lotus-utils.h" +#include #include #include #include @@ -110,7 +111,10 @@ void mousePressResetThread() { needEngineReset.store(true, std::memory_order_release); g_mouse_clicked.store(true, std::memory_order_release); } else { - LOTUS_WARN("Unexpected message received from mouse socket: " + std::string(buf, n)); + // Clamp for the compiler's benefit: n is already known to be in [1, sizeof(buf)] + // here, but GCC cannot prove it and warns that the length may be a huge size_t. + const size_t len = std::min(static_cast(n), sizeof(buf)); + LOTUS_WARN("Unexpected message received from mouse socket: " + std::string(buf, len)); } } else if (ret < 0 && errno != EINTR) { From 15816c86b52cac490bb572f44227a85cb31ceb59 Mon Sep 17 00:00:00 2001 From: Coolllyn Date: Sun, 13 Sep 2026 16:27:38 +0700 Subject: [PATCH 11/25] remove libx11-dev --- .github/workflows/build.yml | 2 +- .github/workflows/packaging-dev.yml | 2 +- .github/workflows/packaging-release.yml | 2 +- CMakeLists.txt | 1 - README.en.md | 8 ++++---- README.md | 8 ++++---- nix/packages/fcitx5-lotus/default.nix | 2 -- packaging/debian/control | 1 - packaging/rpm/fedora/fcitx5-lotus.spec | 1 - packaging/rpm/opensuse/fcitx5-lotus.spec | 1 - src/CMakeLists.txt | 1 - test/CMakeLists.txt | 2 -- 12 files changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c04641f..c42e241e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Install dependencies run: | - pacman -Syu --noconfirm base-devel clang cmake ninja extra-cmake-modules fmt libuv boost git go libinput systemd-libs extra-cmake-modules libx11 python librsvg + pacman -Syu --noconfirm base-devel clang cmake ninja extra-cmake-modules fmt libuv boost git go libinput systemd-libs extra-cmake-modules python librsvg git config --global --add safe.directory "$GITHUB_WORKSPACE" git config --global --add safe.directory "$GITHUB_WORKSPACE/fcitx5-lotus" git config --global --add safe.directory "$GITHUB_WORKSPACE/fcitx5" diff --git a/.github/workflows/packaging-dev.yml b/.github/workflows/packaging-dev.yml index 4333bf1f..9be21868 100644 --- a/.github/workflows/packaging-dev.yml +++ b/.github/workflows/packaging-dev.yml @@ -173,7 +173,7 @@ jobs: set -euo pipefail pacman -Syu --noconfirm \ cmake extra-cmake-modules gcc go \ - fcitx5 libinput libx11 systemd python \ + fcitx5 libinput systemd python \ make pkgconf zstd librsvg cd /work diff --git a/.github/workflows/packaging-release.yml b/.github/workflows/packaging-release.yml index 812670c5..a561c687 100644 --- a/.github/workflows/packaging-release.yml +++ b/.github/workflows/packaging-release.yml @@ -361,7 +361,7 @@ jobs: set -euo pipefail pacman -Syu --noconfirm \ cmake extra-cmake-modules gcc go \ - fcitx5 libinput libx11 systemd python \ + fcitx5 libinput systemd python \ make pkgconf zstd librsvg cd /work diff --git a/CMakeLists.txt b/CMakeLists.txt index b1036edb..92916579 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,6 @@ find_package(Fcitx5Core ${REQUIRED_FCITX_VERSION} REQUIRED) find_package(Fcitx5ModuleEmoji REQUIRED) find_package(Pthread REQUIRED) find_package(Gettext REQUIRED) -find_package(X11 REQUIRED) include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cmake") add_definitions(-DFCITX_GETTEXT_DOMAIN=\"fcitx5-lotus\") diff --git a/README.en.md b/README.en.md index 4d732725..54f3f5ad 100644 --- a/README.en.md +++ b/README.en.md @@ -67,10 +67,10 @@ If you want to compile the input method from source to contribute or customize: ### Dependencies -- **Debian/Ubuntu:** `sudo apt-get install cmake extra-cmake-modules libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev libinput-dev libudev-dev g++ golang hicolor-icon-theme pkg-config libx11-dev fcitx5-modules-dev python3-qtpy python3-dbus acl librsvg2-bin` -- **Fedora/RHEL:** `sudo dnf install cmake extra-cmake-modules fcitx5-devel libinput-devel libudev-devel gcc-c++ golang hicolor-icon-theme systemd-devel libX11-devel python3-pyside6 python3-QtPy python3-dbus acl librsvg2-tools` -- **openSUSE:** `sudo zypper install cmake kf6-extra-cmake-modules fcitx5-devel libinput-devel systemd-devel gcc-c++ go hicolor-icon-theme systemd-devel libX11-devel python3-pyside6 python3-QtPy python3-dbus-python acl rsvg-convert` -- **Arch:** `sudo pacman -S --needed cmake extra-cmake-modules gcc go git libx11 python make pkgconf acl fcitx5 libinput hicolor-icon-theme python-qtpy python-dbus librsvg` +- **Debian/Ubuntu:** `sudo apt-get install cmake extra-cmake-modules libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev libinput-dev libudev-dev g++ golang hicolor-icon-theme pkg-config fcitx5-modules-dev python3-qtpy python3-dbus acl librsvg2-bin` +- **Fedora/RHEL:** `sudo dnf install cmake extra-cmake-modules fcitx5-devel libinput-devel libudev-devel gcc-c++ golang hicolor-icon-theme systemd-devel python3-pyside6 python3-QtPy python3-dbus acl librsvg2-tools` +- **openSUSE:** `sudo zypper install cmake kf6-extra-cmake-modules fcitx5-devel libinput-devel systemd-devel gcc-c++ go hicolor-icon-theme systemd-devel python3-pyside6 python3-QtPy python3-dbus-python acl rsvg-convert` +- **Arch:** `sudo pacman -S --needed cmake extra-cmake-modules gcc go git python make pkgconf acl fcitx5 libinput hicolor-icon-theme python-qtpy python-dbus librsvg` ### Install diff --git a/README.md b/README.md index 7df9569c..10ee6d0f 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,10 @@ Nếu bạn muốn tự biên dịch bộ gõ từ mã nguồn để đóng góp ### Yêu cầu hệ thống -- **Debian/Ubuntu:** `sudo apt-get install cmake extra-cmake-modules libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev libinput-dev libudev-dev g++ golang hicolor-icon-theme pkg-config libx11-dev fcitx5-modules-dev python3-qtpy python3-dbus acl librsvg2-bin` -- **Fedora/RHEL:** `sudo dnf install cmake extra-cmake-modules fcitx5-devel libinput-devel libudev-devel gcc-c++ golang hicolor-icon-theme systemd-devel libX11-devel python3-pyside6 python3-QtPy python3-dbus acl librsvg2-tools` -- **openSUSE:** `sudo zypper install cmake kf6-extra-cmake-modules fcitx5-devel libinput-devel systemd-devel gcc-c++ go hicolor-icon-theme systemd-devel libX11-devel python3-pyside6 python3-QtPy python3-dbus-python acl rsvg-convert` -- **Arch:** `sudo pacman -S --needed cmake extra-cmake-modules gcc go git libx11 python make pkgconf acl fcitx5 libinput hicolor-icon-theme python-qtpy python-dbus librsvg` +- **Debian/Ubuntu:** `sudo apt-get install cmake extra-cmake-modules libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev libinput-dev libudev-dev g++ golang hicolor-icon-theme pkg-config fcitx5-modules-dev python3-qtpy python3-dbus acl librsvg2-bin` +- **Fedora/RHEL:** `sudo dnf install cmake extra-cmake-modules fcitx5-devel libinput-devel libudev-devel gcc-c++ golang hicolor-icon-theme systemd-devel python3-pyside6 python3-QtPy python3-dbus acl librsvg2-tools` +- **openSUSE:** `sudo zypper install cmake kf6-extra-cmake-modules fcitx5-devel libinput-devel systemd-devel gcc-c++ go hicolor-icon-theme systemd-devel python3-pyside6 python3-QtPy python3-dbus-python acl rsvg-convert` +- **Arch:** `sudo pacman -S --needed cmake extra-cmake-modules gcc go git python make pkgconf acl fcitx5 libinput hicolor-icon-theme python-qtpy python-dbus librsvg` ### Cài đặt diff --git a/nix/packages/fcitx5-lotus/default.nix b/nix/packages/fcitx5-lotus/default.nix index 6251bcd3..64411356 100644 --- a/nix/packages/fcitx5-lotus/default.nix +++ b/nix/packages/fcitx5-lotus/default.nix @@ -12,7 +12,6 @@ kdePackages, libinput, librsvg, - libx11, nix-update-script, pkg-config, python3, @@ -71,7 +70,6 @@ stdenv.mkDerivation (finalAttrs: { fcitx5 kdePackages.extra-cmake-modules libinput - libx11 pythonEnv qt6.qtbase qt6.qtsvg diff --git a/packaging/debian/control b/packaging/debian/control index e8e20870..ada844b7 100644 --- a/packaging/debian/control +++ b/packaging/debian/control @@ -16,7 +16,6 @@ Build-Depends: debhelper-compat (= 13), libinput-dev, libudev-dev, pkg-config, - libx11-dev, python3, librsvg2-bin Standards-Version: 4.6.0 diff --git a/packaging/rpm/fedora/fcitx5-lotus.spec b/packaging/rpm/fedora/fcitx5-lotus.spec index 2b705c1f..8c94215f 100644 --- a/packaging/rpm/fedora/fcitx5-lotus.spec +++ b/packaging/rpm/fedora/fcitx5-lotus.spec @@ -13,7 +13,6 @@ BuildRequires: gettext-devel BuildRequires: cmake(Fcitx5Core) BuildRequires: libinput-devel BuildRequires: pkgconfig(libudev) -BuildRequires: libX11-devel BuildRequires: golang BuildRequires: python3-devel diff --git a/packaging/rpm/opensuse/fcitx5-lotus.spec b/packaging/rpm/opensuse/fcitx5-lotus.spec index b0de74fc..38cd5e09 100644 --- a/packaging/rpm/opensuse/fcitx5-lotus.spec +++ b/packaging/rpm/opensuse/fcitx5-lotus.spec @@ -13,7 +13,6 @@ BuildRequires: glibc-devel BuildRequires: fcitx5-devel BuildRequires: libinput-devel BuildRequires: systemd-devel -BuildRequires: libX11-devel BuildRequires: go BuildRequires: sysuser-tools diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 31eae16e..2c0c6fef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,7 +8,6 @@ target_link_libraries(lotus Fcitx5::Module::Emoji Bamboo::Core Pthread::Pthread - X11::X11 ) target_include_directories(lotus PRIVATE diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fbb2340c..e53f030f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -16,7 +16,6 @@ target_link_libraries(lotus_test_core PRIVATE Fcitx5::Module::Emoji Bamboo::Core Pthread::Pthread - X11::X11 ) # Helper macro to compile standalone headless integration test linked with Lotus engine. @@ -41,7 +40,6 @@ function(add_lotus_headless_test target source labels) Fcitx5::Module::Emoji Bamboo::Core Pthread::Pthread - X11::X11 ) add_test(NAME ${target} COMMAND ${target}) set_tests_properties(${target} PROPERTIES LABELS "${labels}" TIMEOUT 20) From e98f6934c728748f7eee0af0f29d40cedeb97828 Mon Sep 17 00:00:00 2001 From: nguyenphivn Date: Mon, 14 Sep 2026 08:17:02 +0700 Subject: [PATCH 12/25] 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 4109b89e15cc36f18ad8baa07161a58572a0135d Mon Sep 17 00:00:00 2001 From: Coolllyn Date: Mon, 14 Sep 2026 17:58:18 +0700 Subject: [PATCH 13/25] remove fcitx5utils --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 92916579..09011903 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,6 @@ find_package(Gettext REQUIRED) include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cmake") add_definitions(-DFCITX_GETTEXT_DOMAIN=\"fcitx5-lotus\") -find_package(Fcitx5Utils REQUIRED) if (Fcitx5Utils_VERSION VERSION_GREATER_EQUAL "5.1.13") add_definitions(-DLOTUS_USE_MODERN_FCITX_API) message(STATUS "Fcitx5 version ${Fcitx5Utils_VERSION} detected: Using modern API") From 748c6815a7c319a62297e7611bedba0d44d2d5c2 Mon Sep 17 00:00:00 2001 From: Coolllyn <259352926+Coolllyn@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:24:30 +0700 Subject: [PATCH 14/25] Adding VUR (Void linux) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9f593fbb..cc181a8b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -67,6 +67,7 @@ body: - AUR (fcitx5-lotus-git) - Cloudflare Pages (Debian/Ubuntu/Fedora/OpenSUSE) - NixOS + - VUR (Void Linux) - Tự build từ source - Khác validations: From 12892d97f4a96ff33d58005485a816d1ce10a129 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Mon, 14 Sep 2026 19:37:58 +0700 Subject: [PATCH 15/25] add gentoo into issue template --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cc181a8b..e4ac9914 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -68,6 +68,7 @@ body: - Cloudflare Pages (Debian/Ubuntu/Fedora/OpenSUSE) - NixOS - VUR (Void Linux) + - GURU (Gentoo) - Tự build từ source - Khác validations: From 6aa5b9860e31d12ef4545f66829d920d3ded4b0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:15:22 +0000 Subject: [PATCH 16/25] chore(deps): bump github/codeql-action from 4.37.9 to 4.38.0 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.9 to 4.38.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.9...v4.38.0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c42e241e..c8dd1f8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: submodules: true - name: Init CodeQL if: matrix.compiler == 'gcc' - uses: github/codeql-action/init@v4.37.9 + uses: github/codeql-action/init@v4.38.0 with: languages: cpp,go source-root: fcitx5-lotus @@ -89,7 +89,7 @@ jobs: ctest --test-dir fcitx5-lotus/build --output-on-failure --no-tests=error - name: CodeQL Analysis if: matrix.compiler == 'gcc' - uses: github/codeql-action/analyze@v4.37.9 + uses: github/codeql-action/analyze@v4.38.0 with: checkout_path: fcitx5-lotus From a9a9deb4b17247a24219dbe910b427d28f3d06f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:15:34 +0000 Subject: [PATCH 17/25] chore(deps): bump peter-evans/create-pull-request from 6 to 8 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 8. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v8) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/update-nix-hash.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-nix-hash.yaml b/.github/workflows/update-nix-hash.yaml index 028646a0..d5b3eaf4 100644 --- a/.github/workflows/update-nix-hash.yaml +++ b/.github/workflows/update-nix-hash.yaml @@ -77,7 +77,7 @@ jobs: - name: Create Pull Request if: steps.git-check.outputs.changed == 'true' - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 with: base: main commit-message: "nix: update fcitx5-lotus to ${{ steps.version.outputs.version }}" From b497a8c6b9aa630e049414dd13866bb777c1534e Mon Sep 17 00:00:00 2001 From: xoaiPro235 Date: Tue, 15 Sep 2026 22:00:30 +0700 Subject: [PATCH 18/25] fix: remove redundant code --- bamboo/fcitxbambooengine.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bamboo/fcitxbambooengine.go b/bamboo/fcitxbambooengine.go index e6b7938c..5a340fd6 100644 --- a/bamboo/fcitxbambooengine.go +++ b/bamboo/fcitxbambooengine.go @@ -68,15 +68,13 @@ const ( func determineMacroCase(str string) uint8 { var hasLower, hasUpper bool for _, c := range str { - if unicode.IsLetter(c) { - if unicode.IsLower(c) { - hasLower = true - } else if unicode.IsUpper(c) { - hasUpper = true - } - if hasLower && hasUpper { - return VnCaseNoChange - } + if unicode.IsLower(c) { + hasLower = true + } else if unicode.IsUpper(c) { + hasUpper = true + } + if hasLower && hasUpper { + return VnCaseNoChange } } if hasLower { From 1a2974e3019e5ccd6f5497f8b77c6581c9f965b2 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Tue, 15 Sep 2026 22:42:14 +0700 Subject: [PATCH 19/25] fix(engine): prioritize scalable icon search paths over raster directories --- src/lotus-engine.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lotus-engine.cpp b/src/lotus-engine.cpp index 72857bf1..7e48972e 100644 --- a/src/lotus-engine.cpp +++ b/src/lotus-engine.cpp @@ -1221,10 +1221,8 @@ namespace fcitx { // hicolor and breeze fallback directories. LotusIconSearchPaths paths; // hicolor status/apps dirs; SVG preferred, PNG only as raster fallback. - paths.systemDirs = { - "/usr/share/icons/hicolor/22x22/status", "/usr/share/icons/hicolor/24x24/status", "/usr/share/icons/hicolor/scalable/status", - "/usr/share/icons/hicolor/scalable/apps", "/usr/share/icons/hicolor/48x48/apps", - }; + paths.systemDirs = {"/usr/share/icons/hicolor/scalable/apps", "/usr/share/icons/hicolor/scalable/status", "/usr/share/icons/hicolor/22x22/status", + "/usr/share/icons/hicolor/24x24/status"}; paths.fallbackDir = FCITX_LOTUS_ICON_DIR; // compile-time install dir iconCachePath_ = resolveLotusIconPath({iconName, baseIconName}, paths); From 2b51baf7e085def26bc6595075578d4465cb53b0 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Tue, 15 Sep 2026 23:15:45 +0700 Subject: [PATCH 20/25] 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 21/25] 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 ) From 2ca89a59be8a2f966de284f6d53d8c9180f289c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Thu, 17 Sep 2026 13:07:04 +0700 Subject: [PATCH 22/25] fix: defer commit out of key event on dbus frontend for GTK4 apps on Wayland --- src/lotus-state.cpp | 26 ++++++++++++++++++++---- src/lotus-state.h | 48 +++++++++++++++++++++++---------------------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/lotus-state.cpp b/src/lotus-state.cpp index a62df55e..c7bfb352 100644 --- a/src/lotus-state.cpp +++ b/src/lotus-state.cpp @@ -460,15 +460,33 @@ namespace fcitx { } } } - ic_->commitString(pending_commit_string_); - LOTUS_INFO("Commit: " + pending_commit_string_); + const bool dbusDefer = getFrontendName(ic_) == "dbus"; + std::string commitText = std::move(pending_commit_string_); + pending_commit_string_.clear(); + if (dbusDefer) { + auto icRef = ic_->watch(); + deferredCommitTimer_ = engine_->instance()->eventLoop().addTimeEvent(CLOCK_MONOTONIC, now(CLOCK_MONOTONIC), 0, + [this, icRef, commitText = std::move(commitText)](EventSourceTime*, uint64_t) { + deferredCommitTimer_.reset(); + if (auto* ic = icRef.get()) { + ic->commitString(commitText); + LOTUS_INFO("Commit (deferred): " + commitText); + } + replayBufferedKeys(); + return true; + }); + } else { + ic_->commitString(commitText); + LOTUS_INFO("Commit: " + commitText); + } expected_backspaces_ = 0; current_backspace_count_ = 0; - pending_commit_string_.clear(); event.filterAndAccept(); // Filter out the final trigger backspace. is_deleting_.store(false); - replayBufferedKeys(); + if (!dbusDefer) { + replayBufferedKeys(); + } return true; } return false; diff --git a/src/lotus-state.h b/src/lotus-state.h index f72645b6..0f5cb0ce 100644 --- a/src/lotus-state.h +++ b/src/lotus-state.h @@ -20,6 +20,7 @@ #include "lotus-utils.h" #include +#include #include #include @@ -85,29 +86,30 @@ namespace fcitx { friend class LotusEngine; private: - static constexpr size_t MAX_BUFFERED_KEYS = 50; - - LotusEngine* engine_; - InputContext* ic_; - CGoObject lotusEngine_; - std::string oldPreBuffer_; - bool hasHistory_ = false; - int expected_backspaces_ = 0; - int current_backspace_count_ = 0; - std::string pending_commit_string_; - std::string emojiBuffer_; - std::vector emojiCandidates_; - bool waitAck_ = false; - std::vector buffered_keys_; ///< Keystrokes buffered during replacement - bool isPrevSpace_ = false; - bool isPrevHyphen_ = false; - bool shouldCapitalize_ = false; - bool isPrevPunctuation_ = false; - int64_t lastDeactivateTime_ = 0; - int64_t deletionInterruptedAt_ = 0; ///< when deactivate() cut an in-flight replacement (0 = none) - bool wa_chromium_flag = false; - bool tracking_modifier_tap_ = false; ///< Selected modifier held, waiting for consecutive keyup - bool macro_skip_ = false; ///< Macro disabled for the current word + static constexpr size_t MAX_BUFFERED_KEYS = 50; + + LotusEngine* engine_; + InputContext* ic_; + CGoObject lotusEngine_; + std::string oldPreBuffer_; + bool hasHistory_ = false; + int expected_backspaces_ = 0; + int current_backspace_count_ = 0; + std::string pending_commit_string_; + std::unique_ptr deferredCommitTimer_; + std::string emojiBuffer_; + std::vector emojiCandidates_; + bool waitAck_ = false; + std::vector buffered_keys_; ///< Keystrokes buffered during replacement + bool isPrevSpace_ = false; + bool isPrevHyphen_ = false; + bool shouldCapitalize_ = false; + bool isPrevPunctuation_ = false; + int64_t lastDeactivateTime_ = 0; + int64_t deletionInterruptedAt_ = 0; ///< when deactivate() cut an in-flight replacement (0 = none) + bool wa_chromium_flag = false; + bool tracking_modifier_tap_ = false; ///< Selected modifier held, waiting for consecutive keyup + bool macro_skip_ = false; ///< Macro disabled for the current word /** * @brief Connects to the uinput server. From fb7f1d766dc135e0dfba27c39123ea0c41192cfc Mon Sep 17 00:00:00 2001 From: Coolllyn <259352926+Coolllyn@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:29:38 +0000 Subject: [PATCH 23/25] adding ctest for package --- packaging/debian/rules | 2 +- packaging/rpm/fedora/fcitx5-lotus.spec | 4 +++- packaging/rpm/opensuse/fcitx5-lotus.spec | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packaging/debian/rules b/packaging/debian/rules index 49443296..eb86c245 100755 --- a/packaging/debian/rules +++ b/packaging/debian/rules @@ -11,4 +11,4 @@ override_dh_dwz: @echo "Skipping dh_dwz (incompatible with Go/mixed debug sections)" override_dh_auto_configure: - dh_auto_configure -- -DLOTUS_BYTECOMPILE_PYTHON=OFF \ No newline at end of file + dh_auto_configure -- -DLOTUS_BYTECOMPILE_PYTHON=OFF -DBUILD_TESTING=ON diff --git a/packaging/rpm/fedora/fcitx5-lotus.spec b/packaging/rpm/fedora/fcitx5-lotus.spec index 8c94215f..4ac9df7e 100644 --- a/packaging/rpm/fedora/fcitx5-lotus.spec +++ b/packaging/rpm/fedora/fcitx5-lotus.spec @@ -32,7 +32,7 @@ Vietnamese input method for fcitx5 %setup -q %build -%cmake -DLOTUS_BYTECOMPILE_PYTHON=OFF +%cmake -DLOTUS_BYTECOMPILE_PYTHON:BOOL=OFF -DBUILD_TESTING:BOOL=ON %cmake_build %install @@ -40,6 +40,8 @@ Vietnamese input method for fcitx5 %find_lang %{name} %py_byte_compile %{__python3} %{buildroot}%{_datadir}/fcitx5-lotus +%check +%ctest %files -f %{name}.lang %{_datadir}/licenses/%{name}/GPL-3.0-or-later.txt diff --git a/packaging/rpm/opensuse/fcitx5-lotus.spec b/packaging/rpm/opensuse/fcitx5-lotus.spec index 38cd5e09..46de386f 100644 --- a/packaging/rpm/opensuse/fcitx5-lotus.spec +++ b/packaging/rpm/opensuse/fcitx5-lotus.spec @@ -34,7 +34,7 @@ Vietnamese input method for fcitx5 find . -type f -name '*.py' -exec sed -i '1s|^#!.*env python3|#!/usr/bin/python3|' {} + %build -%cmake -DLOTUS_BYTECOMPILE_PYTHON=OFF +%cmake -DLOTUS_BYTECOMPILE_PYTHON:BOOL=OFF -DBUILD_TESTING:BOOL=ON %cmake_build cd %{_builddir}/%{name}-%{version} %sysusers_generate_pre build/misc/user-lotus.conf lotus lotus.conf @@ -127,3 +127,6 @@ fi - Fixed an issue where save failures reported a false success status. - Fixed loading errors by safely skipping malformed application rule entries. - Dynamically scaled sleep delays based on text length to improve responsiveness and smooth text rendering. + +%check +%ctest From c43c02b0a80ad66a956e7739eb8a6958020b1f53 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Fri, 18 Sep 2026 00:58:23 +0700 Subject: [PATCH 24/25] fix(server): drain libinput events unconditionally after libinput_dispatch() --- server/lotus-server.cpp | 47 +++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/server/lotus-server.cpp b/server/lotus-server.cpp index 2c1589f7..15cb760f 100644 --- a/server/lotus-server.cpp +++ b/server/lotus-server.cpp @@ -380,33 +380,34 @@ int main(int argc, char* argv[]) { } // handle mouse (libinput) - if ((fds[1].revents & POLLIN) != 0) { - struct libinput_event* event = nullptr; - - while ((event = libinput_get_event(li_ctx.get_li())) != nullptr) { - enum libinput_event_type type = libinput_event_get_type(event); - - if (type == LIBINPUT_EVENT_POINTER_BUTTON) { - struct libinput_event_pointer* p = libinput_event_get_pointer_event(event); - if (libinput_event_pointer_get_button_state(p) == LIBINPUT_BUTTON_STATE_PRESSED) { - if (addon_fd.is_valid()) { - if (send(addon_fd.get(), "C", 1, MSG_NOSIGNAL | MSG_DONTWAIT) <= 0) { - LotusLogger::instance().warn("Failed to send to mouse flag client, closing connection"); - addon_fd.reset(-1); - } + // libinput_dispatch() may have already consumed the fd in an iteration where + // fds[1].revents was 0 (poll timeout, or another fd woke us up). Always drain + // the queue; libinput_get_event() returns nullptr when it is empty. + struct libinput_event* event = nullptr; + + while ((event = libinput_get_event(li_ctx.get_li())) != nullptr) { + enum libinput_event_type type = libinput_event_get_type(event); + + if (type == LIBINPUT_EVENT_POINTER_BUTTON) { + struct libinput_event_pointer* p = libinput_event_get_pointer_event(event); + if (libinput_event_pointer_get_button_state(p) == LIBINPUT_BUTTON_STATE_PRESSED) { + if (addon_fd.is_valid()) { + if (send(addon_fd.get(), "C", 1, MSG_NOSIGNAL | MSG_DONTWAIT) <= 0) { + LotusLogger::instance().warn("Failed to send to mouse flag client, closing connection"); + addon_fd.reset(-1); } } - } else if (type == LIBINPUT_EVENT_DEVICE_ADDED) { - struct libinput_device* dev = libinput_event_get_device(event); - const char* name = libinput_device_get_name(dev); - LotusLogger::instance().info("Device added: " + std::string(name)); - if (libinput_device_config_tap_get_finger_count(dev) > 0) { - libinput_device_config_tap_set_enabled(dev, LIBINPUT_CONFIG_TAP_ENABLED); - libinput_device_config_tap_set_button_map(dev, LIBINPUT_CONFIG_TAP_MAP_LRM); - } } - libinput_event_destroy(event); + } else if (type == LIBINPUT_EVENT_DEVICE_ADDED) { + struct libinput_device* dev = libinput_event_get_device(event); + const char* name = libinput_device_get_name(dev); + LotusLogger::instance().info("Device added: " + std::string(name)); + if (libinput_device_config_tap_get_finger_count(dev) > 0) { + libinput_device_config_tap_set_enabled(dev, LIBINPUT_CONFIG_TAP_ENABLED); + libinput_device_config_tap_set_button_map(dev, LIBINPUT_CONFIG_TAP_MAP_LRM); + } } + libinput_event_destroy(event); } } LotusLogger::instance().info("Terminating server..."); From 9170462c7d18b95721afca84ea00ff8e067766db Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Sat, 19 Sep 2026 09:20:54 +0700 Subject: [PATCH 25/25] ready bump to 3.5.10 --- CMakeLists.txt | 2 +- packaging/debian/changelog | 19 ++++++++++--------- packaging/rpm/fedora/fcitx5-lotus.spec | 19 ++++++++++--------- packaging/rpm/opensuse/fcitx5-lotus.spec | 19 ++++++++++--------- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c369dd4..e5ac553e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10) -project(fcitx5-lotus VERSION 3.5.9) +project(fcitx5-lotus VERSION 3.5.10) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(BUILD_TESTING "Build Lotus tests" OFF) diff --git a/packaging/debian/changelog b/packaging/debian/changelog index f31d648c..e99d81c4 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,11 +1,12 @@ -fcitx5-lotus (3.5.9-1) unstable; urgency=medium +fcitx5-lotus (3.5.10-1) unstable; urgency=medium - * Fixed an edge case when typing English words containing numbers while macro mode is enabled. - * Fixed an issue where switching back to the default typing mode did not properly clear per-app rules. - * Fixed potential process crashes caused by invalid CGO handles. - * Added protection against data loss during load failures and implemented atomic file saves. - * Fixed an issue where save failures reported a false success status. - * Fixed loading errors by safely skipping malformed application rule entries. - * Dynamically scaled sleep delays based on text length to improve responsiveness and smooth text rendering. + * Added desktop notifications when switching typing modes via the mode menu. + * Fixed typing and key event handling for GTK4 applications on Wayland. + * Fixed focus loss issues in Chromium on X11 when using uinput modes. + * Fixed tray icon coloring to match KDE Plasma panel themes dynamically. + * Improved icon rendering by prioritizing scalable vector assets over raster images. + * Preserved per-app typing mode rules across configuration reloads and input context switches. + * Fixed input engine crashes when initialized without an external dictionary loaded. + * Fixed input lag caused by unhandled mouse/touchpad input events. - -- Nguyen Hoang Ky Thu, 10 Sep 2026 09:27:00 +0700 \ No newline at end of file + -- Nguyen Hoang Ky Sat, 19 Sep 2026 09:19:00 +0700 \ No newline at end of file diff --git a/packaging/rpm/fedora/fcitx5-lotus.spec b/packaging/rpm/fedora/fcitx5-lotus.spec index 4ac9df7e..6773ca94 100644 --- a/packaging/rpm/fedora/fcitx5-lotus.spec +++ b/packaging/rpm/fedora/fcitx5-lotus.spec @@ -1,5 +1,5 @@ Name: fcitx5-lotus -Version: 3.5.9 +Version: 3.5.10 Release: 1 Summary: Vietnamese input method for fcitx5 License: GPL-3.0-or-later @@ -116,11 +116,12 @@ fi %systemd_postun_with_restart fcitx5-lotus-server@.service %changelog -* Thu Sep 10 2026 Nguyen Hoang Ky - 3.5.9-1 -- Fixed an edge case when typing English words containing numbers while macro mode is enabled. -- Fixed an issue where switching back to the default typing mode did not properly clear per-app rules. -- Fixed potential process crashes caused by invalid CGO handles. -- Added protection against data loss during load failures and implemented atomic file saves. -- Fixed an issue where save failures reported a false success status. -- Fixed loading errors by safely skipping malformed application rule entries. -- Dynamically scaled sleep delays based on text length to improve responsiveness and smooth text rendering. +* Sat Sep 19 2026 Nguyen Hoang Ky - 3.5.10-1 +- Added desktop notifications when switching typing modes via the mode menu. +- Fixed typing and key event handling for GTK4 applications on Wayland. +- Fixed focus loss issues in Chromium on X11 when using uinput modes. +- Fixed tray icon coloring to match KDE Plasma panel themes dynamically. +- Improved icon rendering by prioritizing scalable vector assets over raster images. +- Preserved per-app typing mode rules across configuration reloads and input context switches. +- Fixed input engine crashes when initialized without an external dictionary loaded. +- Fixed input lag caused by unhandled mouse/touchpad input events. diff --git a/packaging/rpm/opensuse/fcitx5-lotus.spec b/packaging/rpm/opensuse/fcitx5-lotus.spec index 46de386f..0f8fdd68 100644 --- a/packaging/rpm/opensuse/fcitx5-lotus.spec +++ b/packaging/rpm/opensuse/fcitx5-lotus.spec @@ -1,5 +1,5 @@ Name: fcitx5-lotus -Version: 3.5.9 +Version: 3.5.10 Release: 1 Summary: Vietnamese input method for fcitx5 License: GPL-3.0-or-later @@ -119,14 +119,15 @@ fi %service_del_postun fcitx5-lotus-server@.service %changelog -* Thu Sep 10 2026 Nguyen Hoang Ky - 3.5.9-1 -- Fixed an edge case when typing English words containing numbers while macro mode is enabled. -- Fixed an issue where switching back to the default typing mode did not properly clear per-app rules. -- Fixed potential process crashes caused by invalid CGO handles. -- Added protection against data loss during load failures and implemented atomic file saves. -- Fixed an issue where save failures reported a false success status. -- Fixed loading errors by safely skipping malformed application rule entries. -- Dynamically scaled sleep delays based on text length to improve responsiveness and smooth text rendering. +* Sat Sep 19 2026 Nguyen Hoang Ky - 3.5.10-1 +- Added desktop notifications when switching typing modes via the mode menu. +- Fixed typing and key event handling for GTK4 applications on Wayland. +- Fixed focus loss issues in Chromium on X11 when using uinput modes. +- Fixed tray icon coloring to match KDE Plasma panel themes dynamically. +- Improved icon rendering by prioritizing scalable vector assets over raster images. +- Preserved per-app typing mode rules across configuration reloads and input context switches. +- Fixed input engine crashes when initialized without an external dictionary loaded. +- Fixed input lag caused by unhandled mouse/touchpad input events. %check %ctest