From ad9757ea8afb09a6c3e9c090d973eb0f69c2c748 Mon Sep 17 00:00:00 2001 From: Jerin Philip Date: Tue, 9 Jan 2024 23:41:31 +0530 Subject: [PATCH] Cleanup and import lemonade as ibus-slimt-t8n --- .clang-tidy | 111 ++++++++ .github/workflows/main.yml | 111 ++++++++ .gitignore | 18 ++ CMakeLists.txt | 49 ++++ LICENSE.txt | 339 +++++++++++++++++++++++ README.md | 98 +++++++ assets/bergamot.png | Bin 0 -> 20915 bytes cmake/GetVersionFromFile.cmake | 60 ++++ data/samples.txt | 4 + data/slimt-t8n-config.yaml | 44 +++ docs/ibus-development.md | 66 +++++ ibus-slimt-t8n.version | 1 + ibus-slimt-t8n/CMakeLists.txt | 36 +++ ibus-slimt-t8n/application.cpp | 77 ++++++ ibus-slimt-t8n/application.h | 17 ++ ibus-slimt-t8n/engine_compat.cpp | 218 +++++++++++++++ ibus-slimt-t8n/engine_compat.h | 94 +++++++ ibus-slimt-t8n/gtypes.h | 245 +++++++++++++++++ ibus-slimt-t8n/ibus_config.h.in | 16 ++ ibus-slimt-t8n/logging.h | 5 + ibus-slimt-t8n/main.cpp | 33 +++ ibus-slimt-t8n/slimt-t8n.xml.in | 29 ++ ibus-slimt-t8n/slimt_engine.cpp | 335 +++++++++++++++++++++++ ibus-slimt-t8n/slimt_engine.h | 77 ++++++ ibus-slimt-t8n/test.cpp | 44 +++ ibus-slimt-t8n/translator.cpp | 319 ++++++++++++++++++++++ ibus-slimt-t8n/translator.h | 126 +++++++++ run-clang-format.py | 408 ++++++++++++++++++++++++++++ scripts/ci/e2e.sh | 7 + scripts/ci/format-check.sh | 60 ++++ scripts/git-export.sh | 22 ++ scripts/ibus-slimt-t8n-configure.py | 90 ++++++ 32 files changed, 3159 insertions(+) create mode 100644 .clang-tidy create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 assets/bergamot.png create mode 100644 cmake/GetVersionFromFile.cmake create mode 100644 data/samples.txt create mode 100644 data/slimt-t8n-config.yaml create mode 100644 docs/ibus-development.md create mode 100644 ibus-slimt-t8n.version create mode 100644 ibus-slimt-t8n/CMakeLists.txt create mode 100644 ibus-slimt-t8n/application.cpp create mode 100644 ibus-slimt-t8n/application.h create mode 100644 ibus-slimt-t8n/engine_compat.cpp create mode 100644 ibus-slimt-t8n/engine_compat.h create mode 100644 ibus-slimt-t8n/gtypes.h create mode 100644 ibus-slimt-t8n/ibus_config.h.in create mode 100644 ibus-slimt-t8n/logging.h create mode 100644 ibus-slimt-t8n/main.cpp create mode 100644 ibus-slimt-t8n/slimt-t8n.xml.in create mode 100644 ibus-slimt-t8n/slimt_engine.cpp create mode 100644 ibus-slimt-t8n/slimt_engine.h create mode 100644 ibus-slimt-t8n/test.cpp create mode 100644 ibus-slimt-t8n/translator.cpp create mode 100644 ibus-slimt-t8n/translator.h create mode 100644 run-clang-format.py create mode 100644 scripts/ci/e2e.sh create mode 100644 scripts/ci/format-check.sh create mode 100644 scripts/git-export.sh create mode 100755 scripts/ibus-slimt-t8n-configure.py diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..c83c3a6 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,111 @@ +--- +# Configure clang-tidy for this project. + +# Here is an explanation for why some of the checks are disabled: +# +# -google-readability-namespace-comments: the *_CLIENT_NS is a macro, and +# clang-tidy fails to match it against the initial value. +# +# -modernize-use-trailing-return-type: clang-tidy recommends using +# `auto Foo() -> std::string { return ...; }`, we think the code is less +# readable in this form. +# +# -modernize-return-braced-init-list: We think removing typenames and using +# only braced-init can hurt readability. +# +# -modernize-avoid-c-arrays: We only use C arrays when they seem to be the +# right tool for the job, such as `char foo[] = "hello"`. In these cases, +# avoiding C arrays often makes the code less readable, and std::array is +# not a drop-in replacement because it doesn't deduce the size. +# +# -performance-move-const-arg: This warning requires the developer to +# know/care more about the implementation details of types/functions than +# should be necessary. For example, `A a; F(std::move(a));` will trigger a +# warning IFF `A` is a trivial type (and therefore the move is +# meaningless). It would also warn if `F` accepts by `const&`, which is +# another detail that the caller need not care about. +# +# -readability-redundant-declaration: A friend declaration inside a class +# counts as a declaration, so if we also declare that friend outside the +# class in order to document it as part of the public API, that will +# trigger a redundant declaration warning from this check. +# +# -readability-function-cognitive-complexity: too many false positives with +# clang-tidy-12. We need to disable this check in macros, and that setting +# only appears in clang-tidy-13. +# +# -bugprone-narrowing-conversions: too many false positives around +# `std::size_t` vs. `*::difference_type`. +# +# -bugprone-easily-swappable-parameters: too many false positives. +# +# -bugprone-implicit-widening-of-multiplication-result: too many false positives. +# Almost any expression of the form `2 * variable` or `long x = a_int * b_int;` +# generates an error. +# +# -bugprone-unchecked-optional-access: too many false positives in tests. +# Despite what the documentation says, this warning appears after +# `ASSERT_TRUE(variable)` or `ASSERT_TRUE(variable.has_value())`. +# +Checks: > + -*, + bugprone-*, + google-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -google-readability-braces-around-statements, + -google-readability-namespace-comments, + -google-runtime-references, + -misc-non-private-member-variables-in-classes, + -misc-const-correctness, + -modernize-return-braced-init-list, + -modernize-use-trailing-return-type, + -modernize-use-nodiscard, + -modernize-avoid-c-arrays, + -performance-move-const-arg, + -readability-braces-around-statements, + -readability-identifier-length, + -readability-named-parameter, + -readability-redundant-declaration, + -readability-function-cognitive-complexity, + -bugprone-narrowing-conversions, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-exception-escape, + -bugprone-unchecked-optional-access, + -portability-simd-intrinsics + +# Turn all the warnings from the checks above into errors. +WarningsAsErrors: "*" + +HeaderFilterRegex: "(google/cloud/|generator/).*\\.h$" + +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: lower_case } + - { key: readability-identifier-naming.VariableCase, value: lower_case } + - { key: readability-identifier-naming.ClassMemberCase, value: lower_case } + - { key: readability-identifier-naming.ClassMethodCase, value: lower_case } + - { key: readability-identifier-naming.ClassMemberSuffix, value: _ } + - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } + - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } + - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } + - { key: readability-identifier-naming.ConstexprVariablePrefix, value: k } + - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } + - { key: readability-identifier-naming.GlobalConstantPrefix, value: k } + - { key: readability-identifier-naming.MemberCase, value: lower_case } + - { key: readability-identifier-naming.MemberConstantCase, value: CamelCase } + - { key: readability-identifier-naming.MemberConstantPrefix, value: k } + - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } + - { key: readability-identifier-naming.StaticConstantPrefix, value: k } + - { key: readability-implicit-bool-conversion.AllowIntegerConditions, value: 1 } + - { key: readability-implicit-bool-conversion.AllowPointerConditions, value: 1 } + - { key: readability-function-cognitive-complexity.IgnoreMacros, value: 1 } + - { key: readability-identifier-naming.TemplateParameterIgnoredRegexp, value: 'expr-type'} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..acc712b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,111 @@ +name: "main" +on: + push: + branches: + - main + pull_request: + branches: + - '**' +env: + ccache_basedir: ${{ github.workspace }} + ccache_dir: "${{ github.workspace }}/.ccache" + ccache_compilercheck: content + ccache_compress: 'true' + ccache_compresslevel: 9 + ccache_maxsize: 200M + ccache_cmake: -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache + +jobs: + main: + name: "build-test" + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + - name: Install Dependencies + run: |- + sudo apt-get update + sudo apt-get install -y \ + ccache \ + libibus-1.0-dev \ + libyaml-cpp-dev \ + + - name: Install slimt + run: |- + sudo apt-get install -y libopenblas-dev libsentencepiece-dev + + # libxsimd-dev fails due to some issue. + git clone https://github.com/xtensor-stack/xsimd --branch 11.1.0 --depth 1 + cmake -B xsimd/build -S xsimd + cmake --build xsimd/build --target all + sudo cmake --build xsimd/build --target install + + git clone --recursive https://github.com/jerinphilip/slimt --single-branch --branch main + cmake -B slimt/build -S slimt -DEXPORT_CMAKE_FILE=ON -DUSE_BUILTIN_SENTENCEPIECE=OFF \ + -DWITH_GEMMOLOGY=ON -DUSE_AVX2=ON -DUSE_SSE2=ON -DUSE_SSSE3=ON -DUSE_AVX512=ON \ + -DWITH_INTGEMM=OFF \ + -DSLIMT_PACKAGE=ON + cmake --build slimt/build --target all + sudo cmake --build slimt/build --target install + + - name: Generate ccache_vars for ccache based on machine + shell: bash + id: ccache_vars + run: |- + echo "::set-output name=hash::$(echo ${{ env.ccache_compilercheck }})" + echo "::set-output name=timestamp::$(date '+%Y-%m-%dT%H.%M.%S')" + + - name: Cache-op for build-cache through ccache + uses: actions/cache@v2 + with: + path: ${{ env.ccache_dir }} + key: ccache-${{ matrix.identifier }}-${{ steps.ccache_vars.outputs.hash }}-${{ github.ref }}-${{ steps.ccache_vars.outputs.timestamp }} + restore-keys: |- + ccache-${{ matrix.identifier }}-${{ steps.ccache_vars.outputs.hash }}-${{ github.ref }} + ccache-${{ matrix.identifier }}-${{ steps.ccache_vars.outputs.hash }} + ccache-${{ matrix.identifier }} + - name: ccache environment setup + run: |- + echo "CCACHE_COMPILER_CHECK=${{ env.ccache_compilercheck }}" >> $GITHUB_ENV + echo "CCACHE_BASEDIR=${{ env.ccache_basedir }}" >> $GITHUB_ENV + echo "CCACHE_COMPRESS=${{ env.ccache_compress }}" >> $GITHUB_ENV + echo "CCACHE_COMPRESSLEVEL=${{ env.ccache_compresslevel }}" >> $GITHUB_ENV + echo "CCACHE_DIR=${{ env.ccache_dir }}" >> $GITHUB_ENV + echo "CCACHE_MAXSIZE=${{ env.ccache_maxsize }}" >> $GITHUB_ENV + - name: ccache prolog + run: |- + ccache -s # Print current cache stats + ccache -z # Zero cache entry + - name: cmake + run: |- + mkdir -p build + cd build + cmake -L .. -DCOMPILE_TESTS=on ${{ env.ccache_cmake }} + + - name: Build from source + working-directory: build + run: | + make -j2 + + - name: "Download models (slimt) and configure ibus-slimt-t8n" + run: |- + # Install bergamot, which will manage models. + python3 -m pip install slimt -f https://github.com/jerinphilip/slimt/releases/expanded_assets/latest + + # Download models. + slimt download -m en-fr-tiny + slimt download -m fr-en-tiny + slimt download -m de-en-tiny + slimt download -m en-de-tiny + + python3 scripts/ibus-slimt-t8n-configure.py --default browsermt/en-de-tiny --verify + + - name: Test translator backend + run: |- + ./test fake < ${{ github.workspace }}/data/samples.txt + ./test real < ${{ github.workspace }}/data/samples.txt + + - name: ccache epilog + run: 'ccache -s # Print current cache stats' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bf05d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +build/ +dist/ +.*sw[op] + +lemonade/ibus_config.h +*.egg-info +*.pyc +__pycache__ +env/ + +docs/_build +docs/make.bat + +ibus-slimt-t8n.fossil +*.marks + +.cache +.fslckout diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..91f139d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.5.1) +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +include(CMakeDependentOption) + +if(POLICY CMP0074) + cmake_policy(SET CMP0074 NEW) # CMake 3.12 +endif() +project(ibus-slimt-t8n CXX C) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(GetVersionFromFile) +message(STATUS "Project name: ${PROJECT_NAME}") +message(STATUS "Project version: ${PROJECT_VERSION_STRING_FULL}") + +set(AUTHOR "Jerin Philip ") +set(AUTHOR_XML_ESCAPED "Jerin Philip <jerinphilip@live.in>") +set(PROJECT_SHORTNAME "ibus-slimt-t8n") +set(PROJECT_LONGNAME "slimt-t8n") +set(PROJECT_DESCRIPTION + "slimt-t8n provides client-side translation on the local *nix machine.") +set(PROJECT_VERSION ${PROJECT_VERSION_STRING}) +set(PROJECT_HOMEPAGE "https://github.com/jerinphilip/ibus-slimt-t8n") +set(PROJECT_LICENSE "GPL") + +find_package(yaml-cpp REQUIRED) +find_package(slimt REQUIRED) + +find_package(PkgConfig) +pkg_check_modules(GLIB2 REQUIRED glib-2.0) +pkg_check_modules(IBUS REQUIRED ibus-1.0) + +if(NOT TARGET yaml-cpp::yaml-cpp) + add_library(yaml-cpp::yaml-cpp ALIAS yaml-cpp) +endif() + +set(SLIMT_T8N_PRIVATE_LIBS slimt::slimt-shared yaml-cpp::yaml-cpp + ${GLIB2_LIBRARIES} ${IBUS_LIBRARIES}) + +include(GNUInstallDirs) + +add_subdirectory(ibus-slimt-t8n) + +install(TARGETS ibus-slimt-t8n RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}) +install(FILES ${CMAKE_BINARY_DIR}/slimt-t8n.xml + DESTINATION /usr/share/ibus/component) +install(FILES ${CMAKE_SOURCE_DIR}/assets/bergamot.png + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4468fe3 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# ibus-slimt-t8n + +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) + +Repurposes [Intelligent Input Method +Bus](https://en.wikipedia.org/wiki/Intelligent_Input_Bus) (iBus) to intercept +text entered into a field by a user, and insert translated text in it's place +into any graphical application. Hence, this allows you as a user to enter text +in a language you know, while the field gets the translated text. Useful when +interacting with websites or agents in a foreign language. + +See the software in action below: + +iBus translation in action + +The functionality will work in any GUI application which requests +keyboard-input - browser, text-editors, mail-clients, chat-clients etc. + +## Setup + +Setup requires the following: + + +* Compiler supporting C++17 or higher. +* [slimt](https://github.com/jerinphilip/slimt), which provides a + commodity machine (most CPUs) inference engine for the neural models + powering machine translation. +* ibus and GLIB 2.0 libraries (for GTK and iBus) +* yaml-cpp + +**Installing Dependencies** On ArchLinux, dependencies can be installed by: + +```bash +pacman -S ibus glib2 +pacman -S yaml-cpp +``` + +For [slimt](https://github.com/jerinphilip/slimt), follow instructions in the +repository. Package management (models) is done by _slimt_, and +`ibus-slimt-t8n` is downstream to slimt. The command-line that also manages +packages can be installed via PyPI and can operate standalone. + +```bash +python3 -m pip install slimt +slimt ls +slimt download +``` + +**Building from source** Once things are in place, run the following steps to +build and install `ibus-slimt-t8n` and associated files required for +integration into ibus. + +``` +# Configure. +cmake -B build -S . + +# Build +cmake --build build --target all + +# Install. +sudo cmake --build build --target install +# Install the project... +# -- Install configuration: "Release" +# -- Up-to-date: /usr/local/libexec/ibus-slimt-t8n +# -- Up-to-date: /usr/share/ibus/component/slimt-t8n.xml +# -- Up-to-date: /usr/local/share/icons/bergamot.png +``` + +**First run** For first time install: + +1. Restart GNOME (in case the entry does not appear among available input methods). +2. Restart `ibus-daemon` (`ibus-daemon -rXv`) so the ibus parent updates to be + aware of `slimt-t8n.xml` and is capable of spawning the ibus engine + (`ibus-slimt-t8n`). + +From time to time, you may configure `ibus-slimt-t8n` using the script supplied +([`scripts/ibus-slimt-t8n-configure.py`](./scripts/ibus-slimt-t8n-configure.py), +requires python and [`slimt`](https://pypi.org/project/slimt/) python package), +installed previously. + +```bash +python3 scripts/ibus-slimt-t8n-configure.py \ + --default browsermt/en-de-tiny \ + --verify +``` + +The generated configuration is located at `$HOME/.config/ibus-slimt-t8n.yml` +which can be edited by hand to add your own models, as long the YAML remains +valid. + +**Related Projects** + +* [bergamot-translator](https://github.com/browsermt/bergamot-translator) +* [translateLocally](https://github.com/XapaJIaMnu/translateLocally) +* [slimt](https://github.com/jerinphilip/slimt) +* [lemonade](https://github.com/jerinphilip/lemonade) (This repository is a + rebrand after reducing dependencies and using slimt). + diff --git a/assets/bergamot.png b/assets/bergamot.png new file mode 100644 index 0000000000000000000000000000000000000000..62b5711d111e40f5118d90d63b4449751f277dd6 GIT binary patch literal 20915 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 Language and Region**
+ +* Search for "slimt-t8n", add it among input sources. If you don't see this + entry, try logging out of GNOME and back in again. diff --git a/ibus-slimt-t8n.version b/ibus-slimt-t8n.version new file mode 100644 index 0000000..77d6f4c --- /dev/null +++ b/ibus-slimt-t8n.version @@ -0,0 +1 @@ +0.0.0 diff --git a/ibus-slimt-t8n/CMakeLists.txt b/ibus-slimt-t8n/CMakeLists.txt new file mode 100644 index 0000000..db3bee6 --- /dev/null +++ b/ibus-slimt-t8n/CMakeLists.txt @@ -0,0 +1,36 @@ +set(IBUS_BUS_NAME "org.freedesktop.IBus.slimt") +set(IBUS_ENGINE_NAME "slimt-t8n") +set(IBUS_ENGINE_EXECUTABLE_NAME "ibus-${IBUS_ENGINE_NAME}") +set(IBUS_COMMANDLINE + "${CMAKE_INSTALL_LIBEXECDIR}/${IBUS_ENGINE_EXECUTABLE_NAME}") +set(IBUS_TEXTDOMAIN "ibus-slimt") +set(IBUS_ICON "${CMAKE_INSTALL_FULL_DATAROOTDIR}/icons/bergamot.png") +set(IBUS_LAYOUT "us") +set(IBUS_LANGUAGE "en") + +set(IBUS_FULL_COMMANDLINE + "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBEXECDIR}/${IBUS_ENGINE_EXECUTABLE_NAME} --ibus" +) + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/${IBUS_ENGINE_NAME}.xml.in" + "${CMAKE_BINARY_DIR}/${IBUS_ENGINE_NAME}.xml") + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ibus_config.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/ibus_config.h" @ONLY) + +add_library(slimt-t8n STATIC engine_compat.cpp slimt_engine.cpp translator.cpp + application.cpp) +target_link_libraries(slimt-t8n PUBLIC ${SLIMT_T8N_PRIVATE_LIBS}) + +target_include_directories( + slimt-t8n PUBLIC $ + $) + +target_include_directories(slimt-t8n PUBLIC ${GLIB2_INCLUDE_DIRS} + ${IBUS_INCLUDE_DIRS}) + +add_executable(${IBUS_ENGINE_EXECUTABLE_NAME} main.cpp) +target_link_libraries(${IBUS_ENGINE_EXECUTABLE_NAME} PUBLIC slimt-t8n) + +add_executable(test test.cpp) +target_link_libraries(test PUBLIC slimt-t8n) diff --git a/ibus-slimt-t8n/application.cpp b/ibus-slimt-t8n/application.cpp new file mode 100644 index 0000000..abcf565 --- /dev/null +++ b/ibus-slimt-t8n/application.cpp @@ -0,0 +1,77 @@ +#include "ibus-slimt-t8n/application.h" +#include "ibus-slimt-t8n/logging.h" + +namespace ibus::slimt::t8n { + +Application::Application(gboolean ibus) { + ibus_init(); + + // TODO(jerin): Bus can be g::Object derived. + bus_ = ibus_bus_new(); + + if (!ibus_bus_is_connected(bus_.get())) { + LOG("Cannot connect to ibus!"); + g_warning("Can not connect to ibus!"); + std::abort(); + } + + if (!ibus_bus_get_config(bus_.get())) { + LOG("IBus config component is not ready!"); + g_warning("IBus config component is not ready!"); + std::abort(); + } + + auto callback = +[](IBusBus *, gpointer) { ibus_quit(); }; + + g_signal_connect(bus_.get(), "disconnected", G_CALLBACK(callback), NULL); + + LOG("Adding factory"); + factory_ = ibus_factory_new(ibus_bus_get_connection(bus_.get())); + + ibus_factory_add_engine(factory_.get(), PROJECT_SHORTNAME, + IBUS_TYPE_SLIMT_T8N_ENGINE); + + if (ibus) { + LOG("ibus = true, requesting bus"); + ibus_bus_request_name(bus_.get(), IBUS_BUS_NAME, 0); + } else { + LOG("ibus = false, creating new bus"); + g::Holder component( // + ibus_component_new( // + IBUS_BUS_NAME, // + PROJECT_DESCRIPTION, // + PROJECT_VERSION, // + PROJECT_LICENSE, // + AUTHOR, // + PROJECT_HOMEPAGE, // + IBUS_COMPONENT_COMMANDLINE, // + IBUS_TEXTDOMAIN // + )); + + if (component.get()) { + LOG("creating component success"); + } + + g::Holder description( // + ibus_engine_desc_new( // + PROJECT_SHORTNAME, // + PROJECT_LONGNAME, // + PROJECT_DESCRIPTION, // + IBUS_LANGUAGE, // + PROJECT_LICENSE, // + AUTHOR, // + IBUS_ICON, // + IBUS_LAYOUT // + )); + + ibus_component_add_engine(component.get(), description.get()); + ibus_bus_register_component(bus_.get(), component.get()); + } +} + +void Application::run() { + LOG("Spawning ibus main"); + ibus_main(); + LOG("Ending ibus main"); +} +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/application.h b/ibus-slimt-t8n/application.h new file mode 100644 index 0000000..3718853 --- /dev/null +++ b/ibus-slimt-t8n/application.h @@ -0,0 +1,17 @@ +#pragma once +#include "ibus-slimt-t8n/engine_compat.h" +#include "ibus-slimt-t8n/ibus_config.h" +#include +#include + +namespace ibus::slimt::t8n { +class Application { +public: + explicit Application(gboolean ibus); + static void run(); + +private: + g::Holder bus_{nullptr}; + g::Holder factory_{nullptr}; +}; +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/engine_compat.cpp b/ibus-slimt-t8n/engine_compat.cpp new file mode 100644 index 0000000..ab0094a --- /dev/null +++ b/ibus-slimt-t8n/engine_compat.cpp @@ -0,0 +1,218 @@ +#include "ibus-slimt-t8n/engine_compat.h" +#include "ibus-slimt-t8n/slimt_engine.h" +#include + +namespace ibus::slimt::t8n { + +/* code of engine class of GObject */ +#define IBUS_SLIMT_T8N_ENGINE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), IBUS_TYPE_SLIMT_T8N_ENGINE, \ + IBusSlimtEngine)) +#define IBUS_SLIMT_T8N_ENGINE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), IBUS_TYPE_SLIMT_T8N_ENGINE, \ + IBusSlimtEngineClass)) +#define IBUS_IS_SLIMT_T8N_ENGINE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), IBUS_TYPE_SLIMT_T8N_ENGINE)) +#define IBUS_IS_SLIMT_T8N_ENGINE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), IBUS_TYPE_SLIMT_T8N_ENGINE)) +#define IBUS_SLIMT_T8N_ENGINE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), IBUS_TYPE_SLIMT_T8N_ENGINE, \ + IBusSlimtEngineClass)) + +using IBusSlimtEngine = struct IBusSlimtEngine; +using IBusSlimtEngineClass = struct IBusSlimtEngineClass; + +struct IBusSlimtEngineClass { + IBusEngineClass parent; +}; + +struct IBusSlimtEngine { + IBusEngine parent; + + /* members */ + Engine *engine; +}; + +/* functions prototype */ +static void ibus_slimt_t8n_engine_class_init(IBusSlimtEngineClass *klass); +static void ibus_slimt_t8n_engine_init(IBusSlimtEngine *slimt_t8n); +static GObject * +ibus_slimt_t8n_engine_constructor(GType type, guint n_construct_params, + GObjectConstructParam *construct_params); + +static void ibus_slimt_t8n_engine_destroy(IBusSlimtEngine *slimt_t8n); +static gboolean ibus_slimt_t8n_engine_process_key_event(IBusEngine *engine, + guint keyval, + guint keycode, + guint modifiers); +static void ibus_slimt_t8n_engine_focus_in(IBusEngine *engine); +static void ibus_slimt_t8n_engine_focus_out(IBusEngine *engine); +#if IBUS_CHECK_VERSION(1, 5, 4) +static void ibus_slimt_t8n_engine_set_content_type(IBusEngine *engine, + guint purpose, guint hints); +#endif +static void ibus_slimt_t8n_engine_reset(IBusEngine *engine); +static void ibus_slimt_t8n_engine_enable(IBusEngine *engine); +static void ibus_slimt_t8n_engine_disable(IBusEngine *engine); + +#if 0 +static void ibus_engine_set_cursor_location (IBusEngine *engine, + gint x, + gint y, + gint w, + gint h); +static void ibus_slimt_t8n_engine_set_capabilities + (IBusEngine *engine, + guint caps); +#endif + +static void ibus_slimt_t8n_engine_page_up(IBusEngine *engine); +static void ibus_slimt_t8n_engine_page_down(IBusEngine *engine); +static void ibus_slimt_t8n_engine_cursor_up(IBusEngine *engine); +static void ibus_slimt_t8n_engine_cursor_down(IBusEngine *engine); +static void ibus_slimt_t8n_engine_property_activate(IBusEngine *engine, + const gchar *prop_name, + guint prop_state); +static void ibus_slimt_t8n_engine_candidate_clicked(IBusEngine *engine, + guint index, guint button, + guint state); +#if 0 +static void ibus_slimt_t8n_engine_property_show (IBusEngine *engine, + const gchar *prop_name); +static void ibus_slimt_t8n_engine_property_hide (IBusEngine *engine, + const gchar *prop_name); +#endif + +G_DEFINE_TYPE(IBusSlimtEngine, ibus_slimt_t8n_engine, IBUS_TYPE_ENGINE) + +static void ibus_slimt_t8n_engine_class_init(IBusSlimtEngineClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS(klass); + IBusObjectClass *ibus_object_class = IBUS_OBJECT_CLASS(klass); + IBusEngineClass *engine_class = IBUS_ENGINE_CLASS(klass); + + object_class->constructor = ibus_slimt_t8n_engine_constructor; + ibus_object_class->destroy = + reinterpret_cast(ibus_slimt_t8n_engine_destroy); + + engine_class->process_key_event = ibus_slimt_t8n_engine_process_key_event; + + engine_class->reset = ibus_slimt_t8n_engine_reset; + engine_class->enable = ibus_slimt_t8n_engine_enable; + engine_class->disable = ibus_slimt_t8n_engine_disable; + + engine_class->focus_in = ibus_slimt_t8n_engine_focus_in; + engine_class->focus_out = ibus_slimt_t8n_engine_focus_out; + +#if IBUS_CHECK_VERSION(1, 5, 4) + engine_class->set_content_type = ibus_slimt_t8n_engine_set_content_type; +#endif + + engine_class->page_up = ibus_slimt_t8n_engine_page_up; + engine_class->page_down = ibus_slimt_t8n_engine_page_down; + + engine_class->cursor_up = ibus_slimt_t8n_engine_cursor_up; + engine_class->cursor_down = ibus_slimt_t8n_engine_cursor_down; + + engine_class->property_activate = ibus_slimt_t8n_engine_property_activate; + + engine_class->candidate_clicked = ibus_slimt_t8n_engine_candidate_clicked; +} + +static void ibus_slimt_t8n_engine_init(IBusSlimtEngine *slimt_t8n) { + if (g_object_is_floating(slimt_t8n)) + g_object_ref_sink(slimt_t8n); // make engine sink +} + +static GObject * +ibus_slimt_t8n_engine_constructor(GType type, guint n_construct_params, + GObjectConstructParam *construct_params) { + IBusSlimtEngine *engine; + const gchar *name; + + engine = reinterpret_cast( + G_OBJECT_CLASS(ibus_slimt_t8n_engine_parent_class) + ->constructor(type, n_construct_params, construct_params)); + name = ibus_engine_get_name(reinterpret_cast(engine)); + engine->engine = new SlimtEngine(IBUS_ENGINE(engine)); + return reinterpret_cast(engine); +} + +static void ibus_slimt_t8n_engine_destroy(IBusSlimtEngine *slimt_t8n) { + delete slimt_t8n->engine; + (static_cast(ibus_slimt_t8n_engine_parent_class)) + ->destroy(reinterpret_cast(slimt_t8n)); +} + +static gboolean ibus_slimt_t8n_engine_process_key_event(IBusEngine *engine, + guint keyval, + guint keycode, + guint modifiers) { + auto *slimt_t8n = reinterpret_cast(engine); + return slimt_t8n->engine->process_key_event(keyval, keycode, modifiers); +} + +#if IBUS_CHECK_VERSION(1, 5, 4) +static void ibus_slimt_t8n_engine_set_content_type(IBusEngine *engine, + guint purpose, guint hints) { + auto *slimt_t8n = reinterpret_cast(engine); + return slimt_t8n->engine->set_content_type(purpose, hints); +} +#endif + +static void ibus_slimt_t8n_engine_property_activate(IBusEngine *engine, + const gchar *prop_name, + guint prop_state) { + auto *slimt_t8n = reinterpret_cast(engine); + slimt_t8n->engine->property_activate(prop_name, prop_state); +} +static void ibus_slimt_t8n_engine_candidate_clicked(IBusEngine *engine, + guint index, guint button, + guint state) { + auto *slimt_t8n = reinterpret_cast(engine); + slimt_t8n->engine->candidate_clicked(index, button, state); +} + +#define FUNCTION(name, Name) \ + static void ibus_slimt_t8n_engine_##name(IBusEngine *engine) { \ + IBusSlimtEngine *slimt_t8n = (IBusSlimtEngine *)engine; \ + slimt_t8n->engine->Name(); \ + ((IBusEngineClass *)ibus_slimt_t8n_engine_parent_class)->name(engine); \ + } +FUNCTION(focus_in, focus_in) +FUNCTION(focus_out, focus_out) +FUNCTION(reset, reset) +FUNCTION(enable, enable) +FUNCTION(disable, disable) +FUNCTION(page_up, page_up) +FUNCTION(page_down, page_down) +FUNCTION(cursor_up, cursor_up) +FUNCTION(cursor_down, cursor_down) +#undef FUNCTION + +Engine::Engine(IBusEngine *engine) : engine_holder_(engine), engine_(engine) { +#if IBUS_CHECK_VERSION(1, 5, 4) + m_input_purpose_ = IBUS_INPUT_PURPOSE_FREE_FORM; +#endif +} + +gboolean Engine::content_is_password() { +#if IBUS_CHECK_VERSION(1, 5, 4) + return static_cast(IBUS_INPUT_PURPOSE_PASSWORD == m_input_purpose_); +#else + return FALSE; +#endif +} + +void Engine::focus_out() { +#if IBUS_CHECK_VERSION(1, 5, 4) + m_input_purpose_ = IBUS_INPUT_PURPOSE_FREE_FORM; +#endif +} + +#if IBUS_CHECK_VERSION(1, 5, 4) +void Engine::set_content_type(guint purpose, guint /*hints*/) { + m_input_purpose_ = static_cast(purpose); +} +#endif + +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/engine_compat.h b/ibus-slimt-t8n/engine_compat.h new file mode 100644 index 0000000..1956653 --- /dev/null +++ b/ibus-slimt-t8n/engine_compat.h @@ -0,0 +1,94 @@ +#pragma once +#include + +#include "gtypes.h" + +namespace ibus::slimt::t8n { + +#define IBUS_TYPE_SLIMT_T8N_ENGINE (ibus_slimt_t8n_engine_get_type()) + +GType ibus_slimt_t8n_engine_get_type(); + +class Engine { +public: + explicit Engine(IBusEngine *engine); + virtual ~Engine() = default; + + gboolean content_is_password(); + + // virtual functions + virtual gboolean process_key_event(guint keyval, guint keycode, + guint modifiers) = 0; + virtual void focus_in() = 0; + virtual void focus_out(); +#if IBUS_CHECK_VERSION(1, 5, 4) + virtual void set_content_type(guint purpose, guint hints); +#endif + virtual void reset() = 0; + virtual void enable() = 0; + virtual void disable() = 0; + virtual void page_up() = 0; + virtual void page_down() = 0; + virtual void cursor_up() = 0; + virtual void cursor_down() = 0; + virtual gboolean property_activate(const gchar *prop_name, + guint prop_state) = 0; + virtual void candidate_clicked(guint index, guint button, guint state) = 0; + +protected: + void commit_text(const g::Text &text) const { + ibus_engine_commit_text(engine_, text.get()); + } + + void update_preedit_text(const g::Text &text, guint cursor, + gboolean visible) const { + ibus_engine_update_preedit_text(engine_, text.get(), cursor, visible); + } + + void show_preedit_text() const { ibus_engine_show_preedit_text(engine_); } + + void hide_preedit_text() const { ibus_engine_hide_preedit_text(engine_); } + + void update_auxiliary_text(const g::Text &text, gboolean visible) const { + ibus_engine_update_auxiliary_text(engine_, text.get(), visible); + } + + void show_auxiliary_text() const { ibus_engine_show_auxiliary_text(engine_); } + + void hide_auxiliary_text() const { ibus_engine_hide_auxiliary_text(engine_); } + + void update_lookup_table(const g::LookupTable &table, + gboolean visible) const { + ibus_engine_update_lookup_table(engine_, table.get(), visible); + } + + void update_lookup_table_fast(const g::LookupTable &table, + gboolean visible) const { + ibus_engine_update_lookup_table_fast(engine_, table.get(), visible); + } + + void show_lookup_table() const { ibus_engine_show_lookup_table(engine_); } + + void hide_lookup_table() const { ibus_engine_hide_lookup_table(engine_); } + + static void clear_lookup_table(const g::LookupTable &table) { + ibus_lookup_table_clear(table.get()); + } + + void register_properties(const g::PropList &props) const { + ibus_engine_register_properties(engine_, props.get()); + } + + void update_property(const g::Property &prop) const { + ibus_engine_update_property(engine_, prop.get()); + } + + g::Holder engine_holder_; // engine pointer + IBusEngine *engine_; + +#if IBUS_CHECK_VERSION(1, 5, 4) + IBusInputPurpose m_input_purpose_; +#endif +}; + +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/gtypes.h b/ibus-slimt-t8n/gtypes.h new file mode 100644 index 0000000..836f26a --- /dev/null +++ b/ibus-slimt-t8n/gtypes.h @@ -0,0 +1,245 @@ +#pragma once + +#include +#include +#include + +// This file includes types wrapping the GLIB objects into RAII C++ classes. +// GLIB objects follow some global reference-counting. This is an alteration +// from the original import from https://github.com/libzhuyin/ibus-libzhuyin, +// but appears to work. +// +// Not written in the best of states - expect rough edges. +namespace g { + +// RAII wrap automating some things for a GLIB pointer object +// The following concepts +// exist: +// +// 1. Increase ref-count equivalent to increasing the count of usage in +// std::shared_ptr. +// 2. Decrease ref-count equivalent to reducing count of usage in +// std::shared_ptr. When this hits 0, the allocated object is freed. +// 3. Borrowing (equivalent to .get(), when there is no alteration to +// reference-count, but a pointer is passed around). +// +// +// This behavior is translated into a Holder for a Raw pointer (along the same +// lines as an std::shared_ptr) as +// 1. Construction = g_object_ref_sink(...) +// 2. Destruction = g_object_unrf(....) +// 3. Borrowing = .get() +// +// With the above in place, there is not much need to bother about +// reference-count updates, they happen taking advantage of C++'s RAII, similar +// to the operations of a shared_ptr. +template struct Holder { +public: + explicit Holder(Raw *p = nullptr) : pointer_(nullptr) { set(p); } + ~Holder() { set(nullptr); } + + // Assingment from raw-pointer. + Holder &operator=(Raw *p) { + set(p); + return *this; + } + + // Copy (construction + assignment) + Holder(const Holder &other) { + // Simply set this pointer, incrementing reference. + set(other.pointer_); + }; + + Holder &operator=(const Holder &other) { + if (this != &other) { + // Avoid circular references, set. + set(other.pointer_); + } + return *this; + } + + // Move (construction + assignment) + Holder(Holder &&other) noexcept { + // Set this, unset other. + set(other.pointer_); + other.set(nullptr); + } + + Holder &operator=(Holder &&other) noexcept { + if (this != &other) { + // Avoid circular messups, set this, unset other. + set(other.pointer_); + other.set(nullptr); + } + return *this; + }; + + // Consider different cases of dereferencing a Holder t + + // x = *t; const read + const Raw *operator->() const { return pointer_; } + // t->fn(...) In case t is an object with methods. + Raw *operator->() { return pointer_; } + + // *t = x; not const, write. + // operator Raw *() const { return pointer_; } + Raw *get() const { return pointer_; } + +private: + Raw *pointer_ = nullptr; + + void set(Raw *other) { + if (pointer_) { + auto *g_object_pointer = reinterpret_cast(pointer_); + g_object_unref(g_object_pointer); + } + + pointer_ = other; + if (other) { + g_debug("%s, floating = %d", G_OBJECT_TYPE_NAME(other), + g_object_is_floating(other)); + g_object_ref_sink(other); + } + } +}; + +// All IBUS type wrappers inherit from Object (Holder). A CRTP is used +// to embed the Derived class information at Object for .get(). +template class Object { +public: + explicit Object(Derived *p) : pointer_(reinterpret_cast(p)) { + // g_assert(pointer_.get() != nullptr); + } + + explicit operator GObject *() const { return pointer_.get(); } + + Derived *get() const { return reinterpret_cast(pointer_.get()); } + +private: + Holder pointer_; +}; + +class Text : public Object { +public: + explicit Text(IBusText *text) : Object(text) {} + explicit Text(const gchar *str) : Object(ibus_text_new_from_string(str)) {} + + explicit Text(const std::string &str) + : Object(ibus_text_new_from_string(str.c_str())) {} + + explicit Text(gunichar ch) : Object(ibus_text_new_from_unichar(ch)) {} + + void append_attribute(guint type, guint value, guint start, guint end) { + ibus_text_append_attribute(get(), type, value, start, end); + } + + const gchar *text() const { return get()->text; } +}; + +class StaticText : public Text { +public: + explicit StaticText(const gchar *str) + : Text(ibus_text_new_from_static_string(str)) {} + + explicit StaticText(const std::string &str) + : Text(ibus_text_new_from_static_string(str.c_str())) {} + + explicit StaticText(gunichar ch) : Text(ch) {} +}; + +class LookupTable : public Object { +public: + explicit LookupTable(guint page_size = 10, guint cursor_pos = 0, + gboolean cursor_visible = TRUE, gboolean round = FALSE) + : Object(ibus_lookup_table_new(page_size, cursor_pos, cursor_visible, + round)) {} + + guint page_size() const { return ibus_lookup_table_get_page_size(get()); } + guint orientation() const { return ibus_lookup_table_get_orientation(get()); } + guint cursor_pos() const { return ibus_lookup_table_get_cursor_pos(get()); } + guint size() const { + return ibus_lookup_table_get_number_of_candidates(get()); + } + + gboolean page_up() const { return ibus_lookup_table_page_up(get()); } + gboolean page_down() const { return ibus_lookup_table_page_down(get()); } + gboolean cursor_up() const { return ibus_lookup_table_cursor_up(get()); } + gboolean cursor_down() const { return ibus_lookup_table_cursor_down(get()); } + + void set_page_size(guint size) const { + ibus_lookup_table_set_page_size(get(), size); + } + void set_cursor_pos(guint pos) const { + ibus_lookup_table_set_cursor_pos(get(), pos); + } + void set_orientation(gint orientation) const { + ibus_lookup_table_set_orientation(get(), orientation); + } + void clear() const { ibus_lookup_table_clear(get()); } + void set_cursor_visable(gboolean visable) const { + ibus_lookup_table_set_cursor_visible(get(), visable); + } + void set_label(guint index, IBusText *text) const { + ibus_lookup_table_set_label(get(), index, text); + } + void append_candidate(IBusText *text) const { + ibus_lookup_table_append_candidate(get(), text); + } + void append_label(IBusText *text) const { + ibus_lookup_table_append_label(get(), text); + } + + IBusText *get_candidate(guint index) const { + return ibus_lookup_table_get_candidate(get(), index); + } +}; + +class Property : public Object { +public: + explicit Property(const gchar *key, IBusPropType type = PROP_TYPE_NORMAL, + IBusText *label = nullptr, const gchar *icon = nullptr, + IBusText *tooltip = nullptr, gboolean sensitive = TRUE, + gboolean visible = TRUE, + IBusPropState state = PROP_STATE_UNCHECKED, + IBusPropList *props = nullptr) + : Object(ibus_property_new(key, type, label, icon, tooltip, sensitive, + visible, state, props)) {} + + void set_label(IBusText *text) { ibus_property_set_label(get(), text); } + + void set_label(const gchar *text) { + Text t(text); + set_label(t.get()); + } + + void set_icon(const gchar *icon) { ibus_property_set_icon(get(), icon); } + + void set_symbol(IBusText *text) { ibus_property_set_symbol(get(), text); } + + void set_symbol(const gchar *text) { + Text t(text); + set_symbol(t.get()); + } + + void set_sensitive(gboolean sensitive) { + ibus_property_set_sensitive(get(), sensitive); + } + + void set_tooltip(IBusText *text) { ibus_property_set_tooltip(get(), text); } + + void set_tooltip(const gchar *text) { + Text t(text); + set_tooltip(t.get()); + } +}; + +class PropList : public Object { +public: + PropList() : Object(ibus_prop_list_new()) {} + + void append(const Property &property) { + ibus_prop_list_append(get(), property.get()); + } +}; + +} // namespace g diff --git a/ibus-slimt-t8n/ibus_config.h.in b/ibus-slimt-t8n/ibus_config.h.in new file mode 100644 index 0000000..4763925 --- /dev/null +++ b/ibus-slimt-t8n/ibus_config.h.in @@ -0,0 +1,16 @@ +// clang-format off +#define PROJECT_SHORTNAME "@PROJECT_SHORTNAME@" +#define PROJECT_LONGNAME "@PROJECT_LONGNAME@" +#define PROJECT_DESCRIPTION "@PROJECT_DESCRIPTION@" +#define PROJECT_VERSION "@PROJECT_VERSION@" +#define PROJECT_LICENSE "@PROJECT_LICENSE@" +#define PROJECT_HOMEPAGE "@PROJECT_HOMEPAGE@" +#define AUTHOR "@AUTHOR@" + +#define IBUS_BUS_NAME "@IBUS_BUS_NAME@" +#define IBUS_ICON "@IBUS_ICON@" +#define IBUS_LAYOUT "@IBUS_LAYOUT@" +#define IBUS_LANGUAGE "@IBUS_LANGUAGE@" +#define IBUS_TEXTDOMAIN "@IBUS_TEXTDOMAIN@" +#define IBUS_COMPONENT_COMMANDLINE "@IBUS_COMPONENT_COMMANDLINE" +// clang-format on diff --git a/ibus-slimt-t8n/logging.h b/ibus-slimt-t8n/logging.h new file mode 100644 index 0000000..e6aaa3a --- /dev/null +++ b/ibus-slimt-t8n/logging.h @@ -0,0 +1,5 @@ +#pragma once +#include + +#define APPNAME "ibus-slimt-t8n" +#define LOG(...) g_log(APPNAME, G_LOG_LEVEL_MESSAGE, __VA_ARGS__) diff --git a/ibus-slimt-t8n/main.cpp b/ibus-slimt-t8n/main.cpp new file mode 100644 index 0000000..748735c --- /dev/null +++ b/ibus-slimt-t8n/main.cpp @@ -0,0 +1,33 @@ +#include "ibus-slimt-t8n/application.h" +#include "ibus-slimt-t8n/engine_compat.h" +#include + +int main(int argc, char **argv) { + /* command line options */ + gboolean ibus = FALSE; + gboolean verbose = FALSE; + + const GOptionEntry entries[] = { + {"ibus", 'i', 0, G_OPTION_ARG_NONE, &ibus, + "component is executed by ibus", nullptr}, + {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "verbose", nullptr}, + {nullptr}, + }; + + GError *error = nullptr; + GOptionContext *context; + + /* Parse the command line */ + context = g_option_context_new("- ibus slimt-t8n engine component"); + g_option_context_add_main_entries(context, entries, "ibus-slimt-t8n"); + + if (!g_option_context_parse(context, &argc, &argv, &error)) { + g_print("Option parsing failed: %s\n", error->message); + g_error_free(error); + return (-1); + } + + ibus::slimt::t8n::Application application(ibus); + ibus::slimt::t8n::Application::run(); + return 0; +} diff --git a/ibus-slimt-t8n/slimt-t8n.xml.in b/ibus-slimt-t8n/slimt-t8n.xml.in new file mode 100644 index 0000000..3715880 --- /dev/null +++ b/ibus-slimt-t8n/slimt-t8n.xml.in @@ -0,0 +1,29 @@ + + + + ${IBUS_BUS_NAME} + ${PROJECT_DESCRIPTION} + ${IBUS_FULL_COMMANDLINE} + ${PROJECT_VERSION_STRING_FULL} + ${AUTHOR_XML_ESCAPED} + ${PROJECT_LICENSE} + ${PROJECT_HOMEPAGE} + ${IBUS_TEXTDOMAIN} + + + + ${PROJECT_SHORTNAME} + ${IBUS_LANGUAGE} + ${PROJECT_LICENSE} + ${AUTHOR_XML_ESCAPED} + ${IBUS_ICON} + ${IBUS_LAYOUT} + ${PROJECT_LONGNAME} + ${PROJECT_DESCRIPTION} + 99 + 🍋 + /usr/lib/ibus/ibus-setup-pinyin pinyin + + + + diff --git a/ibus-slimt-t8n/slimt_engine.cpp b/ibus-slimt-t8n/slimt_engine.cpp new file mode 100644 index 0000000..81925b2 --- /dev/null +++ b/ibus-slimt-t8n/slimt_engine.cpp @@ -0,0 +1,335 @@ +#include "ibus-slimt-t8n/slimt_engine.h" +#include "ibus-slimt-t8n/engine_compat.h" +#include +#include +#include +#include +#include + +namespace ibus::slimt::t8n { + +namespace { + +template T8r make() { + auto config = ibus_slimt_t8n_config(); + return T8r(config); +} + +} // namespace + +g::PropList SlimtEngine::make_children(const std::string &side, + const StringSet &languages, + const std::string &default_language) { + bool first = false; + g::PropList properties; + for (const auto &lang : languages) { + std::string key = side + "_" + lang; // NOLINT + g::Text label(lang); + IBusPropState state = + (lang == default_language) ? PROP_STATE_CHECKED : PROP_STATE_UNCHECKED; + + IBusPropList *children = nullptr; + const gchar *icon = nullptr; + gboolean sensitive = TRUE; + gboolean visible = TRUE; + + g::Property property( // + key.c_str(), PROP_TYPE_RADIO, label.get(), icon, label.get(), sensitive, + visible, state, + children // + ); + + properties.append(property); + } + + return properties; +} + +SlimtEngine::Select SlimtEngine::make_select(const std::string &key, // + const std::string &tooltip, // + const StringSet &languages, // + const std::string &value // +) { + const gchar *gkey = key.c_str(); + const gchar *icon = nullptr; + g::Text glabel(key); + g::Text gtooltip(tooltip); + g::PropList gchildren = make_children(key, languages, value); + gboolean sensitive = TRUE; + gboolean visible = TRUE; + + g::Property node(gkey, PROP_TYPE_MENU, glabel.get(), icon, gtooltip.get(), + sensitive, visible, PROP_STATE_CHECKED, gchildren.get()); + + Select select{ + .node = std::move(node), // + .options = std::move(gchildren) // + }; + + return select; +} + +g::Property SlimtEngine::make_verify(bool enable_sensitive) { // + const gchar *icon = nullptr; + g::Text glabel("verify"); + g::Text gtooltip("Verify with backtranslated text as second candidate."); + auto sensitive = static_cast(enable_sensitive); + gboolean visible = TRUE; + IBusPropList *children = nullptr; + g::Property verify("verify", PROP_TYPE_TOGGLE, glabel.get(), icon, + gtooltip.get(), sensitive, visible, PROP_STATE_UNCHECKED, + children); + return verify; +} + +SlimtEngine::UI SlimtEngine::make_ui(Translator &translator) { + + Direction direction = translator.default_direction(); + translator.set_direction(direction); + + Select source = make_select( // + "source", "Source language", // + translator.languages().source, // + direction.source); + Select target = make_select( // + "target", "Target language", // + translator.languages().target, // + direction.target); + + bool enable_sensitive = true; + auto verify = make_verify(enable_sensitive); + + // Assign UI. + return { + .source = std::move(source), // + .target = std::move(target), // + .verify = std::move(verify), // + }; +} + +/* constructor */ +SlimtEngine::SlimtEngine(IBusEngine *engine) + : Engine(engine), translator_(make()), + ui_(make_ui(translator_)) { + LOG("slimt-t8n engine started"); +} + +/* destructor */ +SlimtEngine::~SlimtEngine() { hide_lookup_table(); } + +gboolean SlimtEngine::process_key_event(guint keyval, guint /*keycode*/, + guint modifiers) { + // If both langs are set to equal, translation mechanism needn't kick in. + if (translator_.direction().source == translator_.direction().target) { + return 0; + } + + if (content_is_password()) + return FALSE; + + if (modifiers & IBUS_RELEASE_MASK) { + return FALSE; + } + + // We are skipping any modifiers. Our workflow is simple. Ctrl-Enter key is + // send. + if (modifiers & IBUS_CONTROL_MASK && keyval == IBUS_Return) { + g::Text text(buffer_.target); + commit_text(text); + buffer_.source.clear(); + buffer_.target.clear(); + hide_lookup_table(); + return TRUE; + } + + // If ctrl modifier or something is else, we let it pass + if (modifiers & IBUS_CONTROL_MASK) { + return FALSE; + } + + gboolean retval = FALSE; + switch (keyval) { + case IBUS_space: { + if (buffer_.source.empty()) { + update_buffer(" "); + commit(); + } else if (buffer_.source.back() == ' ') { + commit(); + } else { + update_buffer(" "); + retval = TRUE; + } + + } break; + case IBUS_Return: { + if (buffer_.target.empty()) { + // We have no use for empty enters. + return 0; + } + buffer_.target += "\n"; + commit(); + retval = TRUE; + + } break; + case IBUS_BackSpace: { + if (buffer_.source.empty()) { + // Let the backspace through. + retval = FALSE; + } else { + buffer_.source.pop_back(); + refresh_translation(); + retval = TRUE; + } + } break; + case IBUS_Left: + case IBUS_Right: + case IBUS_Up: + case IBUS_Down: + return FALSE; + break; + + default: { + if (isprint(static_cast(keyval))) { + std::string append; + append += static_cast(keyval); + update_buffer(append); + retval = TRUE; + } else { + retval = FALSE; + } + } break; + } + return retval; +} + +void SlimtEngine::update_buffer(const std::string &append) { + buffer_.source += append; + refresh_translation(); +} + +void SlimtEngine::refresh_translation() { + if (!buffer_.source.empty()) { + std::string translation = translator_.translate(buffer_.source); + buffer_.target = translation; + std::vector entries = {buffer_.source}; + if (translator_.verify()) { + std::string backtranslation = translator_.backtranslate(translation); + entries.push_back(backtranslation); + } + g::LookupTable table = generate_lookup_table(entries); + update_lookup_table(table, + /*visible=*/static_cast(!entries.empty())); + + cursor_position_ = buffer_.target.size(); + g::Text pre_edit(buffer_.target); + update_preedit_text(pre_edit, cursor_position_, /*visible=*/TRUE); + show_lookup_table(); + } else { + // Buffer is already clear (empty). + // We will manually clear the buffer_.target. + buffer_.target.clear(); + + cursor_position_ = buffer_.target.size(); + g::Text pre_edit(buffer_.target); + update_preedit_text(pre_edit, cursor_position_, /*visible=*/FALSE); + hide_preedit_text(); + + hide_lookup_table(); + } +} + +void SlimtEngine::commit() { + g::Text text(buffer_.target); + commit_text(text); + hide_lookup_table(); + + buffer_.source.clear(); + buffer_.target.clear(); + + hide_lookup_table(); + cursor_position_ = 0; + g::Text pre_edit(""); + update_preedit_text(pre_edit, cursor_position_, TRUE); +} + +void SlimtEngine::focus_in() { + g::PropList properties; + properties.append(ui_.source.node); + properties.append(ui_.target.node); + properties.append(ui_.verify); + register_properties(properties); +} + +void SlimtEngine::focus_out() { + buffer_.source.clear(); + buffer_.target.clear(); + Engine::focus_out(); +} + +void SlimtEngine::reset() {} + +void SlimtEngine::enable() {} + +void SlimtEngine::disable() {} + +void SlimtEngine::page_up() {} + +void SlimtEngine::page_down() {} + +void SlimtEngine::cursor_up() {} + +void SlimtEngine::cursor_down() {} + +inline void SlimtEngine::show_setup_dialog() { + // g_spawn_command_line_async(LIBEXECDIR "/ibus-setup-libzhuyin zhuyin", + // NULL); +} + +gboolean SlimtEngine::property_activate(const char *cprop_name, + guint prop_state) { + std::string prop_name(cprop_name); + Direction direction = translator_.direction(); + if (prop_name == "verify") { + LOG("Verify translation is %d -> %d", translator_.verify(), prop_state); + bool verify = (prop_state != 0U); + LOG("Enabling backtranslation %s -> %s", direction.target.c_str(), + direction.source.c_str()); + if (translator_.verifiable()) { + translator_.set_verify(verify); + } + } else { + const std::string &serialized(prop_name); + constexpr size_t kPrefixLength = 6; + constexpr size_t kSeparatorLength = 1; + std::string side = serialized.substr(0, kPrefixLength); + std::string lang = + serialized.substr(kPrefixLength + kSeparatorLength, serialized.size()); + if (prop_state == 1) { + LOG("%s [%s] [%s]", prop_name.c_str(), side.c_str(), lang.c_str()); + if (side == "source") { + direction.source = lang; + } else { + direction.target = lang; + } + translator_.set_direction(direction); + + ui_.verify = make_verify(translator_.verifiable()); + update_property(ui_.verify); + } + } + return FALSE; +} + +void SlimtEngine::candidate_clicked(guint index, guint button, guint state) {} + +g::LookupTable +SlimtEngine::generate_lookup_table(const std::vector &entries) { + g::LookupTable lookup_table; + for (const auto &entry : entries) { + g::Text text(entry); + lookup_table.append_candidate(text.get()); + } + return lookup_table; +} + +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/slimt_engine.h b/ibus-slimt-t8n/slimt_engine.h new file mode 100644 index 0000000..07cd887 --- /dev/null +++ b/ibus-slimt-t8n/slimt_engine.h @@ -0,0 +1,77 @@ +#pragma once + +#include "ibus-slimt-t8n/engine_compat.h" +#include "ibus-slimt-t8n/translator.h" +#include +#include +#include + +namespace ibus::slimt::t8n { + +/// Idea here is to maintain an active buffer string. +// +// 1. The first suggestion is the translated text. +// 2. The second suggestion is the raw text the user entered. +class SlimtEngine : public Engine { +public: + explicit SlimtEngine(IBusEngine *engine); + ~SlimtEngine() override; + + // virtual functions + gboolean process_key_event(guint keyval, guint keycode, + guint modifiers) override; + void focus_in() override; + void focus_out() override; + void reset() override; + void enable() override; + void disable() override; + void page_up() override; + void page_down() override; + void cursor_up() override; + void cursor_down() override; + gboolean property_activate(const gchar *prop_name, guint prop_state) override; + void candidate_clicked(guint index, guint button, guint state) override; + +private: + void show_setup_dialog(); + + static g::LookupTable + generate_lookup_table(const std::vector &entries); + + void update_buffer(const std::string &append); + void refresh_translation(); + void commit(); + + Pair buffer_; + gint cursor_position_; + + Translator translator_; + Direction direction_; + + struct Select { + g::Property node; + g::PropList options; + }; + + struct UI { + Select source; + Select target; + g::Property verify; + }; + + UI ui_; + + static UI make_ui(Translator &translator); + static g::PropList make_children(const std::string &side, + const StringSet &languages, + const std::string &default_language); + + static Select make_select(const std::string &key, // + const std::string &tooltip, // + const StringSet &languages, // + const std::string &value); + + static g::Property make_verify(bool enable_sensitive); +}; + +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/test.cpp b/ibus-slimt-t8n/test.cpp new file mode 100644 index 0000000..f38424d --- /dev/null +++ b/ibus-slimt-t8n/test.cpp @@ -0,0 +1,44 @@ +#include "ibus-slimt-t8n/logging.h" +#include "ibus-slimt-t8n/translator.h" +#include +#include + +template void repl(const std::string &config) { + std::cout << "Type in: " + << "\n"; + std::cout << " " + << " \n"; + + std::string input; + using Direction = ibus::slimt::t8n::Direction; + Direction old; + Direction current; + Translator translator(config); + + while (!std::cin.eof()) { + std::cout << " $ "; + std::cin >> current.source; + std::cin >> current.target; + std::getline(std::cin, input); + if (current.source != old.source || current.target != old.target) { + translator.set_direction(current); + old = current; + } + auto translation = translator.translate(input); + std::cout << translation << "\n"; + LOG("Direction %s -> %s: %s / %s", current.source.c_str(), + current.target.c_str(), input.c_str(), translation.c_str()); + } +} + +int main(int argc, char **argv) { + std::string mode((argc == 2) ? argv[1] : ""); + auto config = ibus::slimt::t8n::ibus_slimt_t8n_config(); + if (mode == "fake") { + repl(config); + } else { + repl(config); + } + + return 0; +} diff --git a/ibus-slimt-t8n/translator.cpp b/ibus-slimt-t8n/translator.cpp new file mode 100644 index 0000000..2280a26 --- /dev/null +++ b/ibus-slimt-t8n/translator.cpp @@ -0,0 +1,319 @@ +#include "ibus-slimt-t8n/translator.h" +#include +#include +#include + +#include "yaml-cpp/yaml.h" +#include + +namespace ibus::slimt::t8n { + +Direction reverse(const Direction &direction) { + return { + .source = direction.target, // + .target = direction.source // + }; +} + +Inventory::Inventory(const std::string &config_path) { + inventory_ = load(config_path); + using Strings = std::vector; + auto select_languages = inventory_["languages"].as(); + select_languages_.insert(select_languages.begin(), select_languages.end()); + + YAML::Node models = inventory_["models"]; + for (const YAML::Node &model : models) { + // std::string type = entry["type"].GetString(); + YAML::Node node = model["direction"]; + + Direction direction{ + .source = node["source"].as(), // + .target = node["target"].as() // + }; + + auto preferred = [&, this](const std::string &lang) { + return select_languages_.find(lang) != select_languages_.end(); + }; + + if (preferred(direction.source) and preferred(direction.target)) { + languages_.source.insert(direction.source); + languages_.target.insert(direction.target); + } + + directions_[direction] = model; + } + + default_direction_ = { + .source = inventory_["default"]["source"].as(), // + .target = inventory_["default"]["target"].as() // + }; + + verify_ = inventory_["verify"].as(); +} + +std::shared_ptr make_model(const YAML::Node &config) { + auto root = config["root"].as(); + auto prefix_root = [&root](const std::string &path) { + return root + "/" + path; + }; + + Package path{ + .model = prefix_root(config["model"].as()), // + .vocabulary = + prefix_root(config["vocabs"]["source"].as()), // + .shortlist = prefix_root(config["shortlist"].as()) // + }; + + LOG("model_path: %s", path.model.c_str()); + Model::Config arch = ::slimt::preset::tiny(); + return std::make_shared(arch, path); +} + +std::shared_ptr Inventory::query(const Direction &direction) const { + auto query = directions_.find(direction); + if (query != directions_.end()) { + return make_model(query->second); + } + return nullptr; +} + +const Languages &Inventory::languages() const { return languages_; } + +bool Inventory::exists(const Direction &direction) const { + auto query = directions_.find(direction); + return query != directions_.end(); +} + +const Direction &Inventory::default_direction() const { + return default_direction_; +} + +bool Inventory::Equal::operator()(const Direction &lhs, + const Direction &rhs) const { + return lhs.source == rhs.source && lhs.target == rhs.target; +} + +size_t Inventory::Hash::operator()(const Direction &direction) const { + auto hash_combine = [](size_t &seed, size_t next) { + seed ^= (std::hash{}(next) // + + 0x9e3779b9 // NOLINT + + (seed << 6) // NOLINT + + (seed >> 2) // NOLINT + ); + }; + + size_t seed = std::hash{}(direction.source); + hash_combine(seed, std::hash{}(direction.target)); + return seed; +} + +YAML::Node Inventory::load(const std::string &path) { + YAML::Node tree = YAML::LoadFile(path); + return tree; +} + +void Translator::load_model(const Direction &direction, + Translator::Chain &chain) { + if (direction.source == "English" or direction.target == "English") { + std::shared_ptr model = inventory_.query(direction); + if (model) { + chain.first = model; + LOG("Found model for (%s -> %s)", direction.source.c_str(), + direction.target.c_str()); + } else { + LOG("No model found for %s -> %s", direction.source.c_str(), + direction.target.c_str()); + } + } else { + // Try to translate by pivoting. + Direction to_en{ + .source = direction.source, // + .target = "English" // + }; + + Direction from_en{ + .source = "English", // + .target = direction.target // + }; + + std::shared_ptr first = inventory_.query(to_en); + std::shared_ptr second = inventory_.query(from_en); + + if (first && second) { + chain.first = first; + chain.second = second; + LOG("Found model for (%s -> [en] -> %s)", direction.source.c_str(), + direction.target.c_str()); + } else { + LOG("Unable to generate model (%d) %s -> [en] -> %s %d ", + first == nullptr, direction.source.c_str(), direction.target.c_str(), + second == nullptr); + } + } +} + +void Translator::set_direction(const Direction &direction) { + direction_ = direction; + load_model(direction, forward_); +} + +void Translator::set_verify(bool verify) { + verify_ = verify; + Direction back = reverse(direction_); + load_model(back, backward_); +} + +bool Translator::verifiable() const { + Direction back = reverse(direction_); + if (back.source == "English" or back.target == "English") { + return inventory_.exists(back); + } else { // NOLINT + // Try to translate by pivoting. + Direction to_en{ + .source = back.source, // + .target = "English" // + }; + + Direction from_en{ + .source = "English", // + .target = back.target // + }; + + return inventory_.exists(to_en) and inventory_.exists(from_en); + } +} + +std::string Translator::translate(const std::string &source) { + Options options{.html = false}; + + if (forward_.first && forward_.second) { + // Pivoting. + Handle handle = + service_.pivot(forward_.first, forward_.second, source, options); + Response response = handle.future().get(); + return response.target.text; + } + + assert(forward_.first != nullptr); + + Handle handle = service_.translate(forward_.first, source, options); + Response response = handle.future().get(); + return response.target.text; +} + +std::string Translator::backtranslate(const std::string &source) { + Options options{.html = false}; + if (backward_.first && backward_.second) { + // Pivoting. + Handle handle = + service_.pivot(backward_.first, backward_.second, source, options); + Response response = handle.future().get(); + return response.target.text; + } + + assert(backward_.first != nullptr); + + Handle handle = service_.translate(backward_.first, source, options); + Response response = handle.future().get(); + return response.target.text; +} + +const Languages &Translator::languages() const { + return inventory_.languages(); +} + +const Direction &Translator::default_direction() const { + return inventory_.default_direction(); +} + +void FakeTranslator::set_direction(const Direction &direction) { + direction_ = direction; +} + +void FakeTranslator::set_verify(bool verify) { verify_ = verify; } + +std::string FakeTranslator::translate(std::string input) { // NOLINT + + std::string response; + if (input.empty()) { + return response; + } + + // For a given length, generates a 6 length set of tokens. + // Entire string is changed by seeding with length each time. + // Simulates translation in some capacity. + auto transform = [](size_t length) -> std::string { + std::mt19937_64 generator; + constexpr size_t kTruncateLength = 6; + generator.seed(length); + std::string target; + for (size_t i = 0; i < length; i++) { + if (i != 0) { + target += " "; + } + size_t value = generator(); + constexpr size_t kMaxLength = 20; + std::string hex(kMaxLength, ' '); + std::sprintf(hex.data(), "%x", static_cast(value)); + // 2 to truncate 0x. + target += hex.substr(2, kTruncateLength); + } + return target; + }; + + auto token_count = [](const std::string &input) -> size_t { + std::string token; + size_t count = 0; + for (char c : input) { + if (isspace(c)) { + // Check for space. + if (!token.empty()) { + // Start of a new word. + ++count; + token = ""; + } + } else { + token += std::string(1, c); + } + } + // Non space-detected overhang. + if (!token.empty()) { + count += 1; + } + + return count; + }; + + size_t count = token_count(input); + std::string target = transform(count); + return target; +} + +std::string FakeTranslator::backtranslate(std::string input) { + return translate(std::move(input)); +} + +const Languages &FakeTranslator::languages() const { return languages_; } + +const Direction &FakeTranslator::default_direction() const { + return direction_; +} + +std::string ibus_slimt_t8n_config() { + namespace fs = std::filesystem; + fs::path home = std::getenv("HOME"); + + // Setup logging + // fs::path log_path = home / ".local" / "var" / "ibus-slimt-t8n.log"; + // setup_logging(log_path.string()); + // g_log("ibus-slimt-t8n", // + // G_LOG_LEVEL_MESSAGE, // + // "Creating log at: %s", log_path.string().c_str()); + + // Pickup config-defaults. + fs::path config = home / ".config"; + auto path = (config / "ibus-slimt-t8n.yml").string(); + return path; +} + +} // namespace ibus::slimt::t8n diff --git a/ibus-slimt-t8n/translator.h b/ibus-slimt-t8n/translator.h new file mode 100644 index 0000000..b8f49e9 --- /dev/null +++ b/ibus-slimt-t8n/translator.h @@ -0,0 +1,126 @@ +#pragma once +#include "ibus-slimt-t8n/logging.h" +#include "slimt/slimt.hh" +#include "yaml-cpp/yaml.h" +#include +#include +#include + +namespace ibus::slimt::t8n { + +template struct Pair { + Field source; + Field target; +}; + +using Direction = Pair; +using Strings = std::vector; +using StringSet = std::set; +using Languages = Pair; + +template using Package = ::slimt::Package; +using Config = ::slimt::Config; +using Model = ::slimt::Model; +using Async = ::slimt::Async; +using Options = ::slimt::Options; +using Handle = ::slimt::Handle; +using Response = ::slimt::Response; + +Direction reverse(const Direction &direction); + +class Inventory { +public: + explicit Inventory(const std::string &config_path); + std::shared_ptr query(const Direction &direction) const; + const Languages &languages() const; + bool verify() const { return verify_; } + bool exists(const Direction &direction) const; + const Direction &default_direction() const; + +private: + struct Hash { + size_t operator()(const Direction &direction) const; + }; + + struct Equal { + bool operator()(const Direction &lhs, const Direction &rhs) const; + }; + + std::unordered_map directions_; + std::set select_languages_; + Languages languages_; + Direction default_direction_; + YAML::Node inventory_; + bool verify_; + static YAML::Node load(const std::string &path); +}; + +class Translator { +public: + explicit Translator(const std::string &ibus_config_path) + : service_(Config{}), inventory_(ibus_config_path), + verify_(inventory_.verify()) {} + + void set_direction(const Direction &direction); + void set_verify(bool verify); + bool verify() const { return verify_; } + bool verifiable() const; + const Direction &direction() const { return direction_; } + + std::string translate(const std::string &source); + std::string backtranslate(const std::string &source); + + const Direction &default_direction() const; + const Languages &languages() const; + +private: + using ModelPtr = std::shared_ptr; + using Chain = std::pair; + + void load_model(const Direction &direction, Chain &chain); + + Inventory inventory_; + Direction direction_; + + Async service_; + + Chain forward_; + Chain backward_; + + bool verify_; +}; + +class FakeTranslator { +public: + explicit FakeTranslator(const std::string &){}; + + void set_direction(const Direction &direction); + void set_verify(bool verify); + + bool verify() const { return verify_; } + const Direction &direction() const { return direction_; } + + std::string translate(std::string input); + std::string backtranslate(std::string input); + + const Direction &default_direction() const; + const Languages &languages() const; + +private: + Languages languages_ = { + {"English", "German", "French"}, // + {"English", "German", "French"} // + }; + + Direction direction_{ + .source = "English", // + .target = "German" // + }; + + bool verify_ = false; +}; + +void make_translator(); +std::string ibus_slimt_t8n_config(); + +} // namespace ibus::slimt::t8n diff --git a/run-clang-format.py b/run-clang-format.py new file mode 100644 index 0000000..dcabaf1 --- /dev/null +++ b/run-clang-format.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +"""A wrapper script around clang-format, suitable for linting multiple files +and to use for continuous integration. + +This is an alternative API for the clang-format command line. +It runs over multiple files and directories in parallel. +A diff output is produced and a sensible exit code is returned. + +""" + +from __future__ import print_function, unicode_literals + +import argparse +import codecs +import difflib +import fnmatch +import io +import errno +import multiprocessing +import os +import signal +import subprocess +import sys +import traceback + +from functools import partial + +try: + from subprocess import DEVNULL # py3k +except ImportError: + DEVNULL = open(os.devnull, "wb") + + +DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' +DEFAULT_CLANG_FORMAT_IGNORE = '.clang-format-ignore' + + +class ExitStatus: + SUCCESS = 0 + DIFF = 1 + TROUBLE = 2 + +def excludes_from_file(ignore_file): + excludes = [] + try: + with io.open(ignore_file, 'r', encoding='utf-8') as f: + for line in f: + if line.startswith('#'): + # ignore comments + continue + pattern = line.rstrip() + if not pattern: + # allow empty lines + continue + excludes.append(pattern) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return excludes; + +def list_files(files, recursive=False, extensions=None, exclude=None): + if extensions is None: + extensions = [] + if exclude is None: + exclude = [] + + out = [] + for file in files: + if recursive and os.path.isdir(file): + for dirpath, dnames, fnames in os.walk(file): + fpaths = [os.path.join(dirpath, fname) for fname in fnames] + for pattern in exclude: + # os.walk() supports trimming down the dnames list + # by modifying it in-place, + # to avoid unnecessary directory listings. + dnames[:] = [ + x for x in dnames + if + not fnmatch.fnmatch(os.path.join(dirpath, x), pattern) + ] + fpaths = [ + x for x in fpaths if not fnmatch.fnmatch(x, pattern) + ] + for f in fpaths: + ext = os.path.splitext(f)[1][1:] + if ext in extensions: + out.append(f) + else: + out.append(file) + return out + + +def make_diff(file, original, reformatted): + return list( + difflib.unified_diff( + original, + reformatted, + fromfile='{}\t(original)'.format(file), + tofile='{}\t(reformatted)'.format(file), + n=3)) + + +class DiffError(Exception): + def __init__(self, message, errs=None): + super(DiffError, self).__init__(message) + self.errs = errs or [] + + +class UnexpectedError(Exception): + def __init__(self, message, exc=None): + super(UnexpectedError, self).__init__(message) + self.formatted_traceback = traceback.format_exc() + self.exc = exc + + +def run_clang_format_diff_wrapper(args, file): + try: + ret = run_clang_format_diff(args, file) + return ret + except DiffError: + raise + except Exception as e: + raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__, + e), e) + + +def run_clang_format_diff(args, file): + try: + with io.open(file, 'r', encoding='utf-8') as f: + original = f.readlines() + except IOError as exc: + raise DiffError(str(exc)) + + if args.in_place: + invocation = [args.clang_format_executable, '-i', file] + else: + invocation = [args.clang_format_executable, file] + + if args.style: + invocation.extend(['--style', args.style]) + + if args.dry_run: + print(" ".join(invocation)) + return [], [] + + # Use of utf-8 to decode the process output. + # + # Hopefully, this is the correct thing to do. + # + # It's done due to the following assumptions (which may be incorrect): + # - clang-format will returns the bytes read from the files as-is, + # without conversion, and it is already assumed that the files use utf-8. + # - if the diagnostics were internationalized, they would use utf-8: + # > Adding Translations to Clang + # > + # > Not possible yet! + # > Diagnostic strings should be written in UTF-8, + # > the client can translate to the relevant code page if needed. + # > Each translation completely replaces the format string + # > for the diagnostic. + # > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation + # + # It's not pretty, due to Python 2 & 3 compatibility. + encoding_py3 = {} + if sys.version_info[0] >= 3: + encoding_py3['encoding'] = 'utf-8' + + try: + proc = subprocess.Popen( + invocation, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + **encoding_py3) + except OSError as exc: + raise DiffError( + "Command '{}' failed to start: {}".format( + subprocess.list2cmdline(invocation), exc + ) + ) + proc_stdout = proc.stdout + proc_stderr = proc.stderr + if sys.version_info[0] < 3: + # make the pipes compatible with Python 3, + # reading lines should output unicode + encoding = 'utf-8' + proc_stdout = codecs.getreader(encoding)(proc_stdout) + proc_stderr = codecs.getreader(encoding)(proc_stderr) + # hopefully the stderr pipe won't get full and block the process + outs = list(proc_stdout.readlines()) + errs = list(proc_stderr.readlines()) + proc.wait() + if proc.returncode: + raise DiffError( + "Command '{}' returned non-zero exit status {}".format( + subprocess.list2cmdline(invocation), proc.returncode + ), + errs, + ) + if args.in_place: + return [], errs + return make_diff(file, original, outs), errs + + +def bold_red(s): + return '\x1b[1m\x1b[31m' + s + '\x1b[0m' + + +def colorize(diff_lines): + def bold(s): + return '\x1b[1m' + s + '\x1b[0m' + + def cyan(s): + return '\x1b[36m' + s + '\x1b[0m' + + def green(s): + return '\x1b[32m' + s + '\x1b[0m' + + def red(s): + return '\x1b[31m' + s + '\x1b[0m' + + for line in diff_lines: + if line[:4] in ['--- ', '+++ ']: + yield bold(line) + elif line.startswith('@@ '): + yield cyan(line) + elif line.startswith('+'): + yield green(line) + elif line.startswith('-'): + yield red(line) + else: + yield line + + +def print_diff(diff_lines, use_color): + if use_color: + diff_lines = colorize(diff_lines) + if sys.version_info[0] < 3: + sys.stdout.writelines((l.encode('utf-8') for l in diff_lines)) + else: + sys.stdout.writelines(diff_lines) + + +def print_trouble(prog, message, use_colors): + error_text = 'error:' + if use_colors: + error_text = bold_red(error_text) + print("{}: {} {}".format(prog, error_text, message), file=sys.stderr) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '--clang-format-executable', + metavar='EXECUTABLE', + help='path to the clang-format executable', + default='clang-format') + parser.add_argument( + '--extensions', + help='comma separated list of file extensions (default: {})'.format( + DEFAULT_EXTENSIONS), + default=DEFAULT_EXTENSIONS) + parser.add_argument( + '-r', + '--recursive', + action='store_true', + help='run recursively over directories') + parser.add_argument( + '-d', + '--dry-run', + action='store_true', + help='just print the list of files') + parser.add_argument( + '-i', + '--in-place', + action='store_true', + help='format file instead of printing differences') + parser.add_argument('files', metavar='file', nargs='+') + parser.add_argument( + '-q', + '--quiet', + action='store_true', + help="disable output, useful for the exit code") + parser.add_argument( + '-j', + metavar='N', + type=int, + default=0, + help='run N clang-format jobs in parallel' + ' (default number of cpus + 1)') + parser.add_argument( + '--color', + default='auto', + choices=['auto', 'always', 'never'], + help='show colored diff (default: auto)') + parser.add_argument( + '-e', + '--exclude', + metavar='PATTERN', + action='append', + default=[], + help='exclude paths matching the given glob-like pattern(s)' + ' from recursive search') + parser.add_argument( + '--style', + help='formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)') + + args = parser.parse_args() + + # use default signal handling, like diff return SIGINT value on ^C + # https://bugs.python.org/issue14229#msg156446 + signal.signal(signal.SIGINT, signal.SIG_DFL) + try: + signal.SIGPIPE + except AttributeError: + # compatibility, SIGPIPE does not exist on Windows + pass + else: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + + colored_stdout = False + colored_stderr = False + if args.color == 'always': + colored_stdout = True + colored_stderr = True + elif args.color == 'auto': + colored_stdout = sys.stdout.isatty() + colored_stderr = sys.stderr.isatty() + + version_invocation = [args.clang_format_executable, str("--version")] + try: + subprocess.check_call(version_invocation, stdout=DEVNULL) + except subprocess.CalledProcessError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + return ExitStatus.TROUBLE + except OSError as e: + print_trouble( + parser.prog, + "Command '{}' failed to start: {}".format( + subprocess.list2cmdline(version_invocation), e + ), + use_colors=colored_stderr, + ) + return ExitStatus.TROUBLE + + retcode = ExitStatus.SUCCESS + + excludes = excludes_from_file(DEFAULT_CLANG_FORMAT_IGNORE) + excludes.extend(args.exclude) + + files = list_files( + args.files, + recursive=args.recursive, + exclude=excludes, + extensions=args.extensions.split(',')) + + if not files: + return + + njobs = args.j + if njobs == 0: + njobs = multiprocessing.cpu_count() + 1 + njobs = min(len(files), njobs) + + if njobs == 1: + # execute directly instead of in a pool, + # less overhead, simpler stacktraces + it = (run_clang_format_diff_wrapper(args, file) for file in files) + pool = None + else: + pool = multiprocessing.Pool(njobs) + it = pool.imap_unordered( + partial(run_clang_format_diff_wrapper, args), files) + pool.close() + while True: + try: + outs, errs = next(it) + except StopIteration: + break + except DiffError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + retcode = ExitStatus.TROUBLE + sys.stderr.writelines(e.errs) + except UnexpectedError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + sys.stderr.write(e.formatted_traceback) + retcode = ExitStatus.TROUBLE + # stop at the first unexpected error, + # something could be very wrong, + # don't process all files unnecessarily + if pool: + pool.terminate() + break + else: + sys.stderr.writelines(errs) + if outs == []: + continue + if not args.quiet: + print_diff(outs, use_color=colored_stdout) + if retcode == ExitStatus.SUCCESS: + retcode = ExitStatus.DIFF + if pool: + pool.join() + return retcode + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/ci/e2e.sh b/scripts/ci/e2e.sh new file mode 100644 index 0000000..529eeb8 --- /dev/null +++ b/scripts/ci/e2e.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -eo pipefail + +cmake -B build -S . -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug +cmake --build build --target all # noroot +sudo cmake --build build --target install diff --git a/scripts/ci/format-check.sh b/scripts/ci/format-check.sh new file mode 100644 index 0000000..bd3152e --- /dev/null +++ b/scripts/ci/format-check.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -eo pipefail +set -x + +VCS=fossil + +function formatting-check-clang-format { + # clang-format + python3 run-clang-format.py --style file -r ibus-slimt-t8n +} + +function formatting-check-clang-tidy { + # clang-tidy + mkdir -p build + ARGS=( + -DCMAKE_EXPORT_COMPILE_COMMANDS=on + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + ) + + cmake -B build -S . "${ARGS[@]}" + set +e + FILES=$(find app ibus-slimt-t8n -type f) + run-clang-tidy -export-fixes build/clang-tidy.ibus-slimt-t8n.yml -fix -format -p build -header-filter="$PWD/ibus-slimt-t8n" ${FILES[@]} + CHECK_STATUS=$? + fossil diff + set -e + return $CHECK_STATUS + +} + +function formatting-check-python { + python3 -m black --diff --check scripts/ + python3 -m isort --profile black --diff --check scripts/ +} + +function formatting-check-sh { + shfmt -i 2 -ci -bn -sr -d scripts/ +} + +function formatting-check-cmake { + set +e + CMAKE_FILES=$(find -name "CMakeLists.txt" -not -path "./3rd-party/*" -not -path "build") + cmake-format ${CMAKE_FILES[@]} --check + CHECK_STATUS=$? + set -e + cmake-format ${CMAKE_FILES[@]} --in-place + fossil diff + return $CHECK_STATUS +} + +function formatting-check-iwyu { + iwyu-tool -p build slimt/* > build/iwyu.out +} + +formatting-check-clang-format +formatting-check-python +formatting-check-sh +formatting-check-cmake +formatting-check-clang-tidy diff --git a/scripts/git-export.sh b/scripts/git-export.sh new file mode 100644 index 0000000..b9189d7 --- /dev/null +++ b/scripts/git-export.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -eo pipefail +set -x + +REMOTE="git@github.com:jerinphilip/ibus-slimt-t8n.git" +FOSSIL=$(realpath ibus-slimt-t8n.fossil) +REPO=$(mktemp -d ibus-slimt-t8n-XXX -p /tmp) +mkdir -p $REPO +git -C $REPO init +git -C $REPO checkout -b trunk +git -C $REPO remote add origin $REMOTE + +fossil export --git --export-marks fossil.marks \ + $FOSSIL | git -C $REPO fast-import \ + --export-marks=git.marks + +# fossil export --git $FOSSIL | git -C $REPO fast-import +git -C $REPO log +git -C $REPO branch -m main +git -C $REPO push origin main --force +rm -rf $REPO diff --git a/scripts/ibus-slimt-t8n-configure.py b/scripts/ibus-slimt-t8n-configure.py new file mode 100755 index 0000000..8684244 --- /dev/null +++ b/scripts/ibus-slimt-t8n-configure.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import io +import os +from argparse import ArgumentParser + +import slimt +import yaml +from slimt import REPOSITORY + + +class IBusSlimtT8nConfig: + def __init__(self): + self.models = [] + self.languages = set() + pass + + def add_model(self, model): + self.models.append(model) + direction = model["direction"] + self.languages.add(direction["source"]) + self.languages.add(direction["target"]) + + def set_default(self, model_info): + self.default = {"source": model_info["src"], "target": model_info["trg"]} + + def set_verify(self, verify): + self.verify = verify + + def export(self, path): + payload = { + "models": self.models, + "languages": list(self.languages), + "default": self.default, + "verify": self.verify, + } + + with open(path, "w+") as fp: + export = yaml.dump(payload, fp) + + +def retrieve(model_info, config_path): + dirname = os.path.dirname(config_path) + with open(config_path) as config_file: + data = yaml.safe_load(config_file) + shortlist = data.get("shortlist", None) + return { + "name": model_info["code"], + "direction": {"source": model_info["src"], "target": model_info["trg"]}, + "root": dirname, + "model": data["models"][0], + "vocabs": {"source": data["vocabs"][0], "target": data["vocabs"][-1]}, + "shortlist": shortlist[0] if shortlist else None, + } + + +if __name__ == "__main__": + parser = ArgumentParser() + repositories = REPOSITORY.available() + parser.add_argument( + "-r", + "--repositories", + nargs="+", + default=repositories, + choices=REPOSITORY.available(), + ) + + parser.add_argument("--default", type=str, required=True) + parser.add_argument("--verify", action="store_true") + + args = parser.parse_args() + config = IBusSlimtT8nConfig() + + for repository in args.repositories: + models = REPOSITORY.models(repository, filter_downloaded=True) + for model in models: + config_path = REPOSITORY.model_config_path(repository, model) + model_info = REPOSITORY.model(repository, model) + field = retrieve(model_info, config_path) + if field["shortlist"] is not None: + config.add_model(field) + + repository, model = args.default.split("/") + default_model_info = REPOSITORY.model(repository, model) + config.set_default(default_model_info) + config.set_verify(args.verify) + + home = os.getenv("HOME") + ibus_slimt_t8n_config_path = os.path.join(home, ".config", "ibus-slimt-t8n.yml") + config.export(ibus_slimt_t8n_config_path) + print("Successfully wrote configuration to", ibus_slimt_t8n_config_path)