diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9f593fbb..e4ac9914 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -67,6 +67,8 @@ body: - AUR (fcitx5-lotus-git) - Cloudflare Pages (Debian/Ubuntu/Fedora/OpenSUSE) - NixOS + - VUR (Void Linux) + - GURU (Gentoo) - Tự build từ source - Khác validations: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c04641f..c8dd1f8f 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" @@ -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 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/.github/workflows/update-nix-hash.yaml b/.github/workflows/update-nix-hash.yaml new file mode 100644 index 00000000..d5b3eaf4 --- /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@v8 + 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/CMakeLists.txt b/CMakeLists.txt index b1036edb..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) @@ -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 ) @@ -28,12 +29,10 @@ 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\") -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") 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/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, 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 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 { 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 85cde5fa..64411356 100644 --- a/nix/packages/fcitx5-lotus/default.nix +++ b/nix/packages/fcitx5-lotus/default.nix @@ -12,7 +12,7 @@ kdePackages, libinput, librsvg, - libx11, + nix-update-script, pkg-config, python3, qt6, @@ -40,6 +40,20 @@ stdenv.mkDerivation (finalAttrs: { 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 @@ -56,20 +70,17 @@ stdenv.mkDerivation (finalAttrs: { fcitx5 kdePackages.extra-cmake-modules libinput - libx11 pythonEnv qt6.qtbase qt6.qtsvg 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 +124,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; }; }) 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/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/debian/copyright b/packaging/debian/copyright index eb76b080..554bbc65 100644 --- a/packaging/debian/copyright +++ b/packaging/debian/copyright @@ -4,13 +4,107 @@ 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 License: GPL-3.0-or-later Files: debian/* Copyright: 2026 Nguyen Hoang Ky License: GPL-3.0-or-later +Files: bamboo/* +Copyright: 2022 CSSlayer + 2018 Luong Thanh Lam + 2026 Nguyen Hoang Ky +License: LGPL-2.1-or-later + +Files: bamboo/bamboo-core/* +Copyright: 2018 Luong Thanh Lam + 2026 Nguyen Hoang Ky +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 it under the terms of the GNU General Public License as published by @@ -24,3 +118,17 @@ 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". 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 2b705c1f..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 @@ -13,7 +13,6 @@ BuildRequires: gettext-devel BuildRequires: cmake(Fcitx5Core) BuildRequires: libinput-devel BuildRequires: pkgconfig(libudev) -BuildRequires: libX11-devel BuildRequires: golang BuildRequires: python3-devel @@ -33,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 @@ -41,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 @@ -115,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 b0de74fc..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 @@ -13,7 +13,6 @@ BuildRequires: glibc-devel BuildRequires: fcitx5-devel BuildRequires: libinput-devel BuildRequires: systemd-devel -BuildRequires: libX11-devel BuildRequires: go BuildRequires: sysuser-tools @@ -35,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 @@ -120,11 +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 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..."); 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/src/lotus-engine.cpp b/src/lotus-engine.cpp index b30222c0..1aae2b46 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,12 +109,21 @@ namespace fcitx { lastCheckMs = now; cachedValue = false; + // KDE Plasma: the tray sits on the panel, painted by the Plasma Style, + // while the portal below reports the application colour scheme. The + // two differ in the default Fedora/Kubuntu look (#374). + if (isKdePlasmaSession(getEnv("XDG_CURRENT_DESKTOP"))) { + if (const auto dark = isPlasmaPanelDark(plasmaThemeSearchPathsFromEnv())) { + cachedValue = *dark; + return cachedValue; + } + } + // GTK_THEME is honored by lightweight DEs that lack the settings // portal; covers XFCE, openbox, etc. with a dark theme. - if (const char* theme = std::getenv("GTK_THEME")) { - std::string t(theme); - std::transform(t.begin(), t.end(), t.begin(), ::tolower); - if (t.find("dark") != std::string::npos) { + if (std::string theme = getEnv("GTK_THEME"); !theme.empty()) { + std::transform(theme.begin(), theme.end(), theme.begin(), ::tolower); + if (theme.find("dark") != std::string::npos) { cachedValue = true; return cachedValue; } @@ -148,8 +158,8 @@ namespace fcitx { // GTK settings file — covers DEs where the dark preference is stored // there instead of being exposed via portal/gsettings. - if (const char* home = std::getenv("HOME")) { - std::ifstream settingsFile(std::string(home) + "/.config/gtk-3.0/settings.ini"); + if (std::string home = getEnv("HOME"); !home.empty()) { + std::ifstream settingsFile(home + "/.config/gtk-3.0/settings.ini"); if (settingsFile.is_open()) { std::string line; while (std::getline(settingsFile, line)) { @@ -198,8 +208,8 @@ namespace fcitx { } LotusEngine::LotusEngine(Instance* instance) : instance_(instance), factory_([this](InputContext& ic) { return new LotusState(this, &ic); }) { //NOLINT - const char* desktop = std::getenv("XDG_CURRENT_DESKTOP"); - isGnome_ = (desktop != nullptr) && std::string(desktop).find("GNOME") != std::string::npos; + std::string desktop = getEnv("XDG_CURRENT_DESKTOP"); + isGnome_ = (!desktop.empty()) && desktop.find("GNOME") != std::string::npos; // emptyCustomKeymap_.customKeymap is implicitly initialized to empty by fcitx::Option default value macro. Init(); { @@ -264,6 +274,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(); @@ -440,9 +451,24 @@ namespace fcitx { updateCharsetAction(event.inputContext()); - setMode(targetMode, event.inputContext()); + 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; - auto* state = ic->propertyFor(&factory_); + 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 @@ -456,7 +482,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 @@ -479,7 +505,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(); @@ -632,6 +660,8 @@ namespace fcitx { setMode(selectedMode.value(), ic); if (selectedMode == LotusMode::Emoji) { state->updateEmojiPreedit(); + } else { + showCycleModeNotification(selectedMode.value(), ic); } } } @@ -768,7 +798,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); @@ -783,8 +819,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; }); } @@ -966,6 +1006,8 @@ namespace fcitx { if (mode == LotusMode::Emoji) { auto* state = ic->propertyFor(&factory_); state->updateEmojiPreedit(); + } else { + showCycleModeNotification(mode, ic); } }; }; @@ -1166,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) { @@ -1189,10 +1231,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); 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) { diff --git a/src/lotus-plasma-theme.cpp b/src/lotus-plasma-theme.cpp new file mode 100644 index 00000000..dbee0658 --- /dev/null +++ b/src/lotus-plasma-theme.cpp @@ -0,0 +1,191 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "lotus-plasma-theme.h" +#include "lotus-utils.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 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 std::string& name, const std::string& fallback) { + std::string value = getEnv(name); + return !value.empty() ? value : fallback; + } + + // Reads "key=value" from "[group]" of a KConfig-style INI file. + static std::optional 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; // NOLINT + ++count; + } + if (count != 3 && count != 4) + return std::nullopt; + return rgb; + } + + // Same rule as xdg-desktop-portal-kde: qGray(background) < 192 is dark. + static std::optional 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 isDarkSystemScheme(const PlasmaThemeSearchPaths& paths) { + if (auto dark = isDarkBackground(readConfigValue(paths, "kdeglobals", "Colors:Window", "BackgroundNormal"))) + return dark; + + // kdeglobals may only name the scheme; its colours live in a data file. + const auto scheme = readConfigValue(paths, "kdeglobals", "General", "ColorScheme"); + if (!scheme || scheme->empty()) + return std::nullopt; + for (const auto& dir : dataSearchDirs(paths)) { + const std::string file = joinPath(dir, "color-schemes/" + *scheme + ".colors"); + if (access(file.c_str(), R_OK) == 0) + return isDarkBackground(readIniValue(file, "Colors:Window", "BackgroundNormal")); + } + return std::nullopt; + } + + PlasmaThemeSearchPaths plasmaThemeSearchPathsFromEnv() { + const std::string home = envOr("HOME", ""); + + PlasmaThemeSearchPaths paths; + paths.configHome = envOr("XDG_CONFIG_HOME", joinPath(home, ".config")); + paths.configDirs = splitPathList(envOr("XDG_CONFIG_DIRS", "/etc/xdg")); + paths.dataHome = envOr("XDG_DATA_HOME", joinPath(home, ".local/share")); + paths.dataDirs = splitPathList(envOr("XDG_DATA_DIRS", "/usr/local/share:/usr/share")); + + // Plasma's session startup puts kdedefaults first in XDG_CONFIG_DIRS; + // add it when fcitx5 was started with a different environment. + const std::string kdeDefaults = joinPath(paths.configHome, "kdedefaults"); + if (std::find(paths.configDirs.begin(), paths.configDirs.end(), kdeDefaults) == paths.configDirs.end()) + paths.configDirs.insert(paths.configDirs.begin(), kdeDefaults); + + return paths; + } + + bool isKdePlasmaSession(const std::string& xdgCurrentDesktop) { + const auto desktops = splitPathList(xdgCurrentDesktop); + return std::find(desktops.begin(), desktops.end(), "KDE") != desktops.end(); + } + + std::optional isPlasmaPanelDark(const PlasmaThemeSearchPaths& paths) { + auto theme = readConfigValue(paths, "plasmarc", "Theme", "name"); + if (!theme || theme->empty()) + theme = "default"; + + // Plasma uses the first installed copy of the theme. A theme with its + // own colors file paints the panel with it; one without (Breeze + // "default") follows the system colour scheme. + for (const auto& dir : dataSearchDirs(paths)) { + const std::string themeDir = joinPath(dir, "plasma/desktoptheme/" + *theme); + struct stat st{}; + if (stat(themeDir.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) + continue; + const std::string colors = joinPath(themeDir, "colors"); + if (access(colors.c_str(), R_OK) == 0) + return isDarkBackground(readIniValue(colors, "Colors:Window", "BackgroundNormal")); + break; + } + return isDarkSystemScheme(paths); + } + +} // namespace fcitx diff --git a/src/lotus-plasma-theme.h b/src/lotus-plasma-theme.h new file mode 100644 index 00000000..20ac7d57 --- /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 std::string& xdgCurrentDesktop); + + /** + * @brief Reports whether the Plasma panel background is dark. + * + * Uses the Plasma Style's own colours when it ships a colors file, and + * the system colour scheme otherwise (Breeze "default" follows it). + * + * @return true for a dark panel, false for a light one, std::nullopt + * when no colour could be found. + */ + std::optional isPlasmaPanelDark(const PlasmaThemeSearchPaths& paths); + +} // namespace fcitx + +#endif // _FCITX5_LOTUS_PLASMA_THEME_H_ diff --git a/src/lotus-state.cpp b/src/lotus-state.cpp index 600424c0..c7bfb352 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; @@ -461,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 1f1df591..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,28 +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; - 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. diff --git a/src/lotus-utils.cpp b/src/lotus-utils.cpp index 349b40b9..9ecc5f21 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; @@ -112,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 91793e72..71b7463c 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. * @@ -109,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 518cd186..f74f2ace 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) @@ -56,6 +54,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 @@ -81,4 +80,4 @@ add_test(NAME emoji_backspace_utf8 COMMAND emoji_backspace_utf8) set_tests_properties(emoji_backspace_utf8 PROPERTIES LABELS "unit;emoji" TIMEOUT 20 -) +) \ No newline at end of file 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; +} 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)