Add Avro plugin support and integrate Bangla keyboards in KeyboardWindow

This commit is contained in:
minhaj14d 2026-06-13 19:43:13 +06:00
parent 736d782bd8
commit fe8d0894a4
30 changed files with 2024 additions and 2 deletions

View File

@ -124,6 +124,7 @@ dependencies {
implementation(libs.splitties.views.dsl.recyclerview)
implementation(libs.splitties.views.recyclerview)
implementation(libs.aboutlibraries.core)
implementation(project(":plugin:avro"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.rules)

View File

@ -24,6 +24,9 @@ import org.fcitx.fcitx5.android.input.dependency.theme
import org.fcitx.fcitx5.android.input.picker.PickerWindow
import org.fcitx.fcitx5.android.input.popup.PopupActionListener
import org.fcitx.fcitx5.android.input.popup.PopupComponent
import org.fcitx.fcitx5.android.input.keyboard.bn.BanglaAvroKeyboard
import org.fcitx.fcitx5.android.input.keyboard.bn.ProbhatKeyboard
import org.fcitx.fcitx5.android.plugin.avro.AvroKeyboardIds
import org.fcitx.fcitx5.android.input.wm.EssentialWindow
import org.fcitx.fcitx5.android.input.wm.InputWindow
import org.fcitx.fcitx5.android.input.wm.InputWindowManager
@ -68,7 +71,9 @@ class KeyboardWindow : InputWindow.SimpleInputWindow<KeyboardWindow>(), Essentia
private val keyboards: HashMap<String, BaseKeyboard> by lazy {
hashMapOf(
TextKeyboard.Name to TextKeyboard(context, theme),
NumberKeyboard.Name to NumberKeyboard(context, theme)
NumberKeyboard.Name to NumberKeyboard(context, theme),
BanglaAvroKeyboard.Name to BanglaAvroKeyboard(context, theme),
ProbhatKeyboard.Name to ProbhatKeyboard(context, theme),
)
}
private var currentKeyboardName = ""
@ -139,7 +144,10 @@ class KeyboardWindow : InputWindow.SimpleInputWindow<KeyboardWindow>(), Essentia
}
override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) {
val targetLayout = when (info.inputType and InputType.TYPE_MASK_CLASS) {
val imeLayout = AvroKeyboardIds.layoutForIme(
fcitx.runImmediately { inputMethodEntryCached.uniqueName }
)
val targetLayout = imeLayout ?: when (info.inputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_NUMBER -> NumberKeyboard.Name
InputType.TYPE_CLASS_PHONE -> NumberKeyboard.Name
else -> TextKeyboard.Name
@ -148,6 +156,14 @@ class KeyboardWindow : InputWindow.SimpleInputWindow<KeyboardWindow>(), Essentia
}
override fun onImeUpdate(ime: InputMethodEntry) {
val layout = AvroKeyboardIds.layoutForIme(ime.uniqueName)
if (layout != null) {
switchLayout(layout, remember = false)
} else if (currentKeyboardName == BanglaAvroKeyboard.Name ||
currentKeyboardName == ProbhatKeyboard.Name
) {
switchLayout(TextKeyboard.Name, remember = false)
}
currentKeyboard?.onInputMethodUpdate(ime)
}

View File

@ -0,0 +1,287 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.fcitx.fcitx5.android.input.keyboard.bn
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import androidx.annotation.Keep
import androidx.core.view.allViews
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.InputMethodEntry
import org.fcitx.fcitx5.android.core.KeyState
import org.fcitx.fcitx5.android.core.KeyStates
import org.fcitx.fcitx5.android.core.KeySym
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.input.keyboard.AlphabetKey
import org.fcitx.fcitx5.android.input.keyboard.AltTextKeyView
import org.fcitx.fcitx5.android.input.keyboard.BackspaceKey
import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard
import org.fcitx.fcitx5.android.input.keyboard.CapsKey
import org.fcitx.fcitx5.android.input.keyboard.CommaKey
import org.fcitx.fcitx5.android.input.keyboard.KeyAction
import org.fcitx.fcitx5.android.input.keyboard.KeyDef
import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance.Variant
import org.fcitx.fcitx5.android.input.keyboard.LanguageKey
import org.fcitx.fcitx5.android.input.keyboard.LayoutSwitchKey
import org.fcitx.fcitx5.android.input.keyboard.ReturnKey
import org.fcitx.fcitx5.android.input.keyboard.SpaceKey
import org.fcitx.fcitx5.android.input.keyboard.SymbolKey
import org.fcitx.fcitx5.android.input.popup.PopupAction
import org.fcitx.fcitx5.android.plugin.avro.AvroKeyboardIds
import splitties.views.imageResource
/**
* Roman QWERTY layout for Bangla Avro phonetic input.
* Keys feed the native Avro engine via [KeyAction.FcitxKeyAction].
*/
@SuppressLint("ViewConstructor")
class BanglaAvroKeyboard(
context: Context,
theme: Theme,
) : BaseKeyboard(context, theme, Layout) {
enum class CapsState { None, Once, Lock }
companion object {
const val Name = AvroKeyboardIds.LAYOUT_AVRO
private val BANGLA_DIGITS = mapOf(
'0' to "", '1' to "", '2' to "", '3' to "", '4' to "",
'5' to "", '6' to "", '7' to "", '8' to "", '9' to "",
)
private val AVRO_PUNCTUATION = mapOf(
"0" to "", "1" to "", "2" to "", "3" to "", "4" to "",
"5" to "", "6" to "", "7" to "", "8" to "", "9" to "",
"." to "",
)
val Layout: List<List<KeyDef>> = listOf(
listOf(
AlphabetKey("Q", "1"),
AlphabetKey("W", "2"),
AlphabetKey("E", "3"),
AlphabetKey("R", "4"),
AlphabetKey("T", "5"),
AlphabetKey("Y", "6"),
AlphabetKey("U", "7"),
AlphabetKey("I", "8"),
AlphabetKey("O", "9"),
AlphabetKey("P", "0"),
),
listOf(
AlphabetKey("A", "@"),
AlphabetKey("S", "#"),
AlphabetKey("D", "&"),
AlphabetKey("F", "*"),
AlphabetKey("G", "-"),
AlphabetKey("H", "+"),
AlphabetKey("J", "="),
AlphabetKey("K", "("),
AlphabetKey("L", ")"),
),
listOf(
CapsKey(),
AlphabetKey("Z", "_"),
AlphabetKey("X", ""),
AlphabetKey("C", "\""),
AlphabetKey("V", "'"),
AlphabetKey("B", ":"),
AlphabetKey("N", ";"),
AlphabetKey("M", "/"),
BackspaceKey(),
),
listOf(
LayoutSwitchKey("?123", ""),
CommaKey(0.1f, Variant.Alternative),
LanguageKey(),
SpaceKey(),
SymbolKey(".", 0.1f, Variant.Alternative),
ReturnKey(),
),
)
}
val caps: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_caps) }
val backspace: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_backspace) }
val lang: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_lang) }
val space: org.fcitx.fcitx5.android.input.keyboard.TextKeyView by lazy { findViewById(R.id.button_space) }
val `return`: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_return) }
private val showLangSwitchKey = AppPrefs.getInstance().keyboard.showLangSwitchKey
@Keep
private val showLangSwitchKeyListener = ManagedPreference.OnChangeListener<Boolean> { _, v ->
updateLangSwitchKey(v)
}
private val keepLettersUppercase by AppPrefs.getInstance().keyboard.keepLettersUppercase
init {
updateLangSwitchKey(showLangSwitchKey.getValue())
showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener)
}
private val textKeys: List<org.fcitx.fcitx5.android.input.keyboard.TextKeyView> by lazy {
allViews.filterIsInstance<org.fcitx.fcitx5.android.input.keyboard.TextKeyView>().toList()
}
private var capsState: CapsState = CapsState.None
private fun transformAlphabet(c: String): String = when (capsState) {
CapsState.None -> c.lowercase()
else -> c.uppercase()
}
private var punctuationMapping: Map<String, String> = mapOf()
private fun transformPunctuation(p: String): String {
AVRO_PUNCTUATION[p]?.let { return it }
return punctuationMapping.getOrDefault(p, p)
}
override fun onAction(action: KeyAction, source: org.fcitx.fcitx5.android.input.keyboard.KeyActionListener.Source) {
var transformed = action
when (action) {
is KeyAction.FcitxKeyAction -> when (source) {
org.fcitx.fcitx5.android.input.keyboard.KeyActionListener.Source.Keyboard -> {
val digit = action.act.singleOrNull()?.takeIf(Char::isDigit)
if (digit != null) {
super.onAction(
KeyAction.CommitAction(BANGLA_DIGITS[digit] ?: action.act),
source
)
return
}
when (capsState) {
CapsState.None -> transformed = action.copy(act = action.act.lowercase())
CapsState.Once -> {
transformed = action.copy(
act = action.act.uppercase(),
states = KeyStates(KeyState.Virtual, KeyState.Shift),
)
switchCapsState()
}
CapsState.Lock -> transformed = action.copy(
act = action.act.uppercase(),
states = KeyStates(KeyState.Virtual, KeyState.CapsLock),
)
}
}
org.fcitx.fcitx5.android.input.keyboard.KeyActionListener.Source.Popup -> {
if (capsState == CapsState.Once) switchCapsState()
}
}
is KeyAction.CapsAction -> switchCapsState(action.lock)
else -> {}
}
super.onAction(transformed, source)
}
override fun onAttach() {
capsState = CapsState.None
updateCapsButtonIcon()
updateAlphabetKeys()
}
override fun onReturnDrawableUpdate(returnDrawable: Int) {
`return`.img.imageResource = returnDrawable
}
override fun onPunctuationUpdate(mapping: Map<String, String>) {
punctuationMapping = mapping
updatePunctuationKeys()
}
override fun onInputMethodUpdate(ime: InputMethodEntry) {
space.mainText.text = buildString {
append(ime.displayName)
ime.subMode.run { label.ifEmpty { name.ifEmpty { null } } }?.let { append(" ($it)") }
}
if (capsState != CapsState.None) switchCapsState()
}
override fun onPopupAction(action: PopupAction) {
val newAction = when (action) {
is PopupAction.PreviewAction -> action.copy(content = transformPopupPreview(action.content))
is PopupAction.PreviewUpdateAction -> action.copy(content = transformPopupPreview(action.content))
is PopupAction.ShowKeyboardAction -> {
when (action.keyboard) {
is KeyDef.Popup.Keyboard.Preset -> {
val label = action.keyboard.label
if (label.length == 1 && label[0].isLetter()) {
action.copy(
keyboard = action.keyboard.copy(label = transformAlphabet(label))
)
} else action
}
is KeyDef.Popup.Keyboard.Explicit -> action
}
}
else -> action
}
super.onPopupAction(newAction)
}
private fun transformPopupPreview(c: String): String {
if (c.length != 1) return c
if (c[0].isLetter()) return transformAlphabet(c)
return transformPunctuation(c)
}
private fun switchCapsState(lock: Boolean = false) {
capsState = if (lock) {
when (capsState) {
CapsState.Lock -> CapsState.None
else -> CapsState.Lock
}
} else {
when (capsState) {
CapsState.None -> CapsState.Once
else -> CapsState.None
}
}
updateCapsButtonIcon()
updateAlphabetKeys()
}
private fun updateCapsButtonIcon() {
caps.img.imageResource = when (capsState) {
CapsState.None -> R.drawable.ic_capslock_none
CapsState.Once -> R.drawable.ic_capslock_once
CapsState.Lock -> R.drawable.ic_capslock_lock
}
}
private fun updateLangSwitchKey(visible: Boolean) {
lang.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun updateAlphabetKeys() {
textKeys.forEach {
if (it.def !is KeyDef.Appearance.AltText) return
it.mainText.text = it.def.displayText.let { str ->
if (str.length != 1 || !str[0].isLetter()) return@forEach
if (keepLettersUppercase) str.uppercase() else transformAlphabet(str)
}
}
}
private fun updatePunctuationKeys() {
textKeys.forEach {
if (it is AltTextKeyView) {
it.def as KeyDef.Appearance.AltText
it.altText.text = transformPunctuation(it.def.altText)
} else {
it.def as KeyDef.Appearance.Text
it.mainText.text = it.def.displayText.let { str ->
if (str.isEmpty() || str[0].isLetter() || str[0].isWhitespace()) return@forEach
transformPunctuation(str)
}
}
}
}
}

View File

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.fcitx.fcitx5.android.input.keyboard.bn
import org.fcitx.fcitx5.android.input.keyboard.KeyAction
import org.fcitx.fcitx5.android.input.keyboard.KeyDef
/** Key that commits a Bengali (or other) string directly to the editor. */
class BengaliCommitKey(
displayText: String,
commitText: String = displayText,
percentWidth: Float = 0.1f,
variant: KeyDef.Appearance.Variant = KeyDef.Appearance.Variant.Normal,
) : KeyDef(
KeyDef.Appearance.Text(
displayText = displayText,
textSize = 20f,
percentWidth = percentWidth,
variant = variant,
),
setOf(
KeyDef.Behavior.Press(KeyAction.CommitAction(commitText))
),
)

View File

@ -0,0 +1,99 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.fcitx.fcitx5.android.input.keyboard.bn
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import androidx.annotation.Keep
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.InputMethodEntry
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.input.keyboard.BackspaceKey
import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard
import org.fcitx.fcitx5.android.input.keyboard.CommaKey
import org.fcitx.fcitx5.android.input.keyboard.KeyDef
import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance.Variant
import org.fcitx.fcitx5.android.input.keyboard.LanguageKey
import org.fcitx.fcitx5.android.input.keyboard.LayoutSwitchKey
import org.fcitx.fcitx5.android.input.keyboard.ReturnKey
import org.fcitx.fcitx5.android.input.keyboard.SpaceKey
import org.fcitx.fcitx5.android.plugin.avro.AvroKeyboardIds
import splitties.views.imageResource
/**
* Probhat fixed-layout Bengali keyboard (Ekushey / OpenBangla normal layer).
* Keys commit Bengali characters directly via [KeyAction.CommitAction].
*/
@SuppressLint("ViewConstructor")
class ProbhatKeyboard(
context: Context,
theme: Theme,
) : BaseKeyboard(context, theme, Layout) {
companion object {
const val Name = AvroKeyboardIds.LAYOUT_PROBHAT
// Normal-layer mappings from OpenBangla Probhat.json (Unicode Probhat).
private val ROW1 = listOf("", "", "", "", "", "", "", "", "", "")
private val ROW2 = listOf("", "", "", "", "", "", "", "", "")
private val ROW3 = listOf("", "", "", "", "", "", "")
val Layout: List<List<KeyDef>> = listOf(
ROW1.map { BengaliCommitKey(it) },
ROW2.map { BengaliCommitKey(it) },
listOf(
BengaliCommitKey("", variant = Variant.Alternative),
*ROW3.map { BengaliCommitKey(it) }.toTypedArray(),
BackspaceKey(),
),
listOf(
LayoutSwitchKey("?123", ""),
CommaKey(0.1f, Variant.Alternative),
LanguageKey(),
SpaceKey(),
BengaliCommitKey("", percentWidth = 0.1f, variant = Variant.Alternative),
ReturnKey(),
),
)
}
val backspace: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_backspace) }
val lang: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_lang) }
val space: org.fcitx.fcitx5.android.input.keyboard.TextKeyView by lazy { findViewById(R.id.button_space) }
val `return`: org.fcitx.fcitx5.android.input.keyboard.ImageKeyView by lazy { findViewById(R.id.button_return) }
private val showLangSwitchKey = AppPrefs.getInstance().keyboard.showLangSwitchKey
@Keep
private val showLangSwitchKeyListener = ManagedPreference.OnChangeListener<Boolean> { _, v ->
updateLangSwitchKey(v)
}
init {
updateLangSwitchKey(showLangSwitchKey.getValue())
showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener)
}
override fun onAttach() = Unit
override fun onReturnDrawableUpdate(returnDrawable: Int) {
`return`.img.imageResource = returnDrawable
}
override fun onPunctuationUpdate(mapping: Map<String, String>) = Unit
override fun onInputMethodUpdate(ime: InputMethodEntry) {
space.mainText.text = buildString {
append(ime.displayName)
ime.subMode.run { label.ifEmpty { name.ifEmpty { null } } }?.let { append(" ($it)") }
}
}
private fun updateLangSwitchKey(visible: Boolean) {
lang.visibility = if (visible) View.VISIBLE else View.GONE
}
}

View File

@ -0,0 +1,51 @@
plugins {
id("org.fcitx.fcitx5.android.app-convention")
id("org.fcitx.fcitx5.android.plugin-app-convention")
id("org.fcitx.fcitx5.android.native-app-convention")
id("org.fcitx.fcitx5.android.build-metadata")
id("org.fcitx.fcitx5.android.data-descriptor")
id("org.fcitx.fcitx5.android.fcitx-component")
}
android {
namespace = "org.fcitx.fcitx5.android.plugin.avro"
defaultConfig {
applicationId = "org.fcitx.fcitx5.android.plugin.avro"
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
targets("avro")
}
}
}
buildFeatures {
resValues = true
}
buildTypes {
release {
resValue("string", "app_name", "@string/app_name_release")
proguardFile("proguard-rules.pro")
}
debug {
resValue("string", "app_name", "@string/app_name_debug")
}
}
packaging {
jniLibs {
excludes += setOf(
"**/libc++_shared.so",
"**/libFcitx5*"
)
}
}
}
dependencies {
implementation(project(":lib:fcitx5"))
implementation(project(":lib:plugin-base"))
}

View File

@ -0,0 +1,4 @@
name: Avro Plugin
components:
- name: plugin
type: plugin

1
plugin/avro/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1 @@
-keep class org.fcitx.fcitx5.android.plugin.avro.** { *; }

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" />
</manifest>

Binary file not shown.

View File

@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.18)
project(fcitx5-android-plugin-avro VERSION ${VERSION_NAME})
# For reproducible build
add_link_options("LINKER:--hash-style=gnu,--build-id=none")
# prefab dependency
find_package(fcitx5 REQUIRED CONFIG)
get_target_property(FCITX5_CMAKE_MODULES fcitx5::cmake INTERFACE_INCLUDE_DIRECTORIES)
set(CMAKE_MODULE_PATH ${FCITX5_CMAKE_MODULES} ${CMAKE_MODULE_PATH})
find_package(ECM MODULE)
find_package(Fcitx5Core MODULE)
find_package(Fcitx5Module MODULE)
include("${FCITX_INSTALL_CMAKECONFIG_DIR}/Fcitx5Utils/Fcitx5CompilerSettings.cmake")
add_fcitx5_addon(avro
avro.cpp
avro.h
avro_parser.cpp
avro_parser.h
)
target_link_libraries(avro PRIVATE Fcitx5::Core)
fcitx5_translate_desktop_file(avro.conf.in avro.conf)
fcitx5_translate_desktop_file(probhat.conf.in probhat.conf)
configure_file(avro-addon.conf.in.in avro-addon.conf.in COPYONLY)
fcitx5_translate_desktop_file("${CMAKE_CURRENT_BINARY_DIR}/avro-addon.conf.in" avro-addon.conf)
install(TARGETS avro DESTINATION "${CMAKE_INSTALL_LIBDIR}/fcitx5" COMPONENT translation)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/avro.conf"
"${CMAKE_CURRENT_BINARY_DIR}/probhat.conf"
DESTINATION "${FCITX_INSTALL_PKGDATADIR}/inputmethod" COMPONENT config)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/avro-addon.conf" RENAME avro.conf
DESTINATION "${FCITX_INSTALL_PKGDATADIR}/addon" COMPONENT config)

View File

@ -0,0 +1,7 @@
[Addon]
Name=Avro Bangla Phonetic
Category=InputMethod
Library=avro
Type=SharedLibrary
OnDemand=True
Configurable=True

View File

@ -0,0 +1,7 @@
[InputMethod]
Name=Avro
Icon=fcitx-avro
Label=Avro
LangCode=bn
Addon=avro
Configurable=True

View File

@ -0,0 +1,127 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "avro.h"
#include <fcitx-utils/key.h>
#include <fcitx-utils/keysym.h>
#include <fcitx/inputpanel.h>
#include <fcitx/userinterface.h>
namespace fcitx {
FCITX_ADDON_FACTORY(AvroFactory)
void AvroEngine::activate(const InputMethodEntry &, InputContextEvent &) { buffer_.clear(); }
void AvroEngine::reset(const InputMethodEntry &, InputContextEvent &) { buffer_.clear(); }
void AvroEngine::clearBuffer(InputContext *ic) {
buffer_.clear();
ic->inputPanel().setClientPreedit(Text());
ic->updateUserInterface(UserInterfaceComponent::InputPanel);
}
void AvroEngine::updatePreedit(InputContext *ic) {
if (buffer_.empty()) {
ic->inputPanel().setClientPreedit(Text());
} else {
ic->inputPanel().setClientPreedit(Text(parser_.parse(buffer_)));
}
ic->updateUserInterface(UserInterfaceComponent::InputPanel);
}
bool AvroEngine::isProbhatEntry(const InputMethodEntry &entry) {
return entry.uniqueName() == "probhat";
}
void AvroEngine::handleProbhatKeyEvent(KeyEvent &keyEvent) {
if (keyEvent.isRelease()) {
return;
}
const KeyStates modifiers{KeyState::Ctrl, KeyState::Alt};
if (keyEvent.key().states() & modifiers) {
return;
}
auto *ic = keyEvent.inputContext();
const auto sym = keyEvent.key().sym();
if (sym == FcitxKey_BackSpace) {
keyEvent.filterAndAccept();
return;
}
if (sym == FcitxKey_Return) {
ic->commitString("\n");
keyEvent.filterAndAccept();
return;
}
if (sym == FcitxKey_space) {
ic->commitString(" ");
keyEvent.filterAndAccept();
return;
}
if (keyEvent.key().isSimple()) {
const auto text = Key::keySymToUTF8(sym);
if (!text.empty()) {
ic->commitString(text);
keyEvent.filterAndAccept();
}
}
}
void AvroEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &keyEvent) {
if (isProbhatEntry(entry)) {
handleProbhatKeyEvent(keyEvent);
return;
}
if (keyEvent.isRelease()) {
return;
}
const KeyStates modifiers{KeyState::Ctrl, KeyState::Alt};
if (keyEvent.key().states() & modifiers) {
return;
}
auto *ic = keyEvent.inputContext();
const auto sym = keyEvent.key().sym();
if (sym == FcitxKey_BackSpace) {
if (buffer_.empty()) {
return;
}
buffer_.pop_back();
updatePreedit(ic);
keyEvent.filterAndAccept();
return;
}
if (sym == FcitxKey_Return || sym == FcitxKey_space) {
if (!buffer_.empty()) {
const auto commit = parser_.parse(buffer_);
ic->commitString(commit + (sym == FcitxKey_space ? " " : "\n"));
clearBuffer(ic);
keyEvent.filterAndAccept();
}
return;
}
if (keyEvent.key().isSimple()) {
const auto ch = static_cast<char>(sym);
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '`' || ch == '^' ||
ch == ',' || ch == '.' || (ch >= '0' && ch <= '9')) {
buffer_.push_back(ch);
updatePreedit(ic);
keyEvent.filterAndAccept();
return;
}
}
}
} // namespace fcitx

View File

@ -0,0 +1,48 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef FCITX_AVRO_AVRO_H
#define FCITX_AVRO_AVRO_H
#include <fcitx/addonfactory.h>
#include <fcitx/addonmanager.h>
#include <fcitx/inputcontext.h>
#include <fcitx/inputmethodengine.h>
#include <fcitx/instance.h>
#include <fcitx/text.h>
#include <fcitx-utils/misc.h>
#include <string>
#include "avro_parser.h"
namespace fcitx {
class AvroEngine : public InputMethodEngine {
public:
explicit AvroEngine(Instance *instance) { FCITX_UNUSED(instance); }
void keyEvent(const InputMethodEntry &entry, KeyEvent &keyEvent) override;
void activate(const InputMethodEntry &entry, InputContextEvent &event) override;
void reset(const InputMethodEntry &entry, InputContextEvent &event) override;
private:
static bool isProbhatEntry(const InputMethodEntry &entry);
void handleProbhatKeyEvent(KeyEvent &keyEvent);
void updatePreedit(InputContext *ic);
void clearBuffer(InputContext *ic);
AvroParser parser_;
std::string buffer_;
};
class AvroFactory : public AddonFactory {
public:
AddonInstance *create(AddonManager *manager) override {
return new AvroEngine(manager->instance());
}
};
} // namespace fcitx
#endif

View File

@ -0,0 +1,150 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "avro_parser.h"
#include <algorithm>
#include <cctype>
namespace fcitx {
namespace {
std::vector<AvroPattern> makePatterns();
std::string makeVowels();
std::string makeConsonants();
std::string makeCaseSensitive();
} // namespace
AvroParser::AvroParser()
: patterns_(makePatterns()), vowel_(makeVowels()), consonant_(makeConsonants()),
caseSensitive_(makeCaseSensitive()) {}
bool AvroParser::isExact(const std::string &needle, const std::string &haystack, int start,
int end, bool negate) {
const bool equal = start >= 0 && end <= static_cast<int>(haystack.size()) &&
haystack.substr(start, end - start) == needle;
return equal ^ negate;
}
bool AvroParser::isVowel(char c) const {
const auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return vowel_.find(lower) != std::string::npos;
}
bool AvroParser::isConsonant(char c) const {
const auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return consonant_.find(lower) != std::string::npos;
}
bool AvroParser::isPunctuation(char c) const { return !(isVowel(c) || isConsonant(c)); }
bool AvroParser::isCaseSensitive(char c) const {
const auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return caseSensitive_.find(lower) != std::string::npos;
}
std::string AvroParser::fixString(const std::string &input) const {
std::string fixed;
fixed.reserve(input.size());
for (auto c : input) {
if (isCaseSensitive(c)) {
fixed.push_back(c);
} else {
fixed.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
}
}
return fixed;
}
std::string AvroParser::parse(const std::string &input) const {
const auto fixed = fixString(input);
std::string output;
output.reserve(fixed.size() * 2);
int cur = 0;
while (cur < static_cast<int>(fixed.size())) {
const int start = cur;
bool matched = false;
for (const auto &pattern : patterns_) {
const int end = cur + static_cast<int>(pattern.find.size());
if (end > static_cast<int>(fixed.size()) ||
fixed.substr(start, pattern.find.size()) != pattern.find) {
continue;
}
const int prev = start - 1;
if (!pattern.rules.empty()) {
for (const auto &rule : pattern.rules) {
bool replace = true;
for (const auto &m : rule.matches) {
const int chk = m.type == "suffix" ? end : prev;
bool cond = false;
if (m.scope == "punctuation") {
cond = (chk < 0 && m.type == "prefix") ||
(chk >= static_cast<int>(fixed.size()) && m.type == "suffix") ||
(chk >= 0 && chk < static_cast<int>(fixed.size()) &&
isPunctuation(fixed[chk]));
} else if (m.scope == "vowel") {
const bool inRange = (chk >= 0 && m.type == "prefix") ||
(chk < static_cast<int>(fixed.size()) &&
m.type == "suffix");
cond = inRange && chk >= 0 && chk < static_cast<int>(fixed.size()) &&
isVowel(fixed[chk]);
} else if (m.scope == "consonant") {
const bool inRange = (chk >= 0 && m.type == "prefix") ||
(chk < static_cast<int>(fixed.size()) &&
m.type == "suffix");
cond = inRange && chk >= 0 && chk < static_cast<int>(fixed.size()) &&
isConsonant(fixed[chk]);
} else if (m.scope == "exact") {
int s = 0;
int e = 0;
if (m.type == "suffix") {
s = end;
e = end + static_cast<int>(m.value.size());
} else {
s = start - static_cast<int>(m.value.size());
e = start;
}
cond = isExact(m.value, fixed, s, e, false);
}
if (((!cond) ^ m.negative)) {
replace = false;
break;
}
}
if (replace) {
output += rule.replace;
cur = end;
matched = true;
break;
}
}
}
if (matched) {
break;
}
output += pattern.replace;
cur = end;
matched = true;
break;
}
if (!matched) {
output.push_back(fixed[cur]);
++cur;
}
}
return output;
}
namespace {
#include "avro_rules.inc"
} // namespace
} // namespace fcitx

View File

@ -0,0 +1,52 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef FCITX5_ANDROID_AVRO_PARSER_H
#define FCITX5_ANDROID_AVRO_PARSER_H
#include <string>
#include <vector>
namespace fcitx {
struct AvroMatchRule {
std::string type;
std::string scope;
std::string value;
bool negative;
};
struct AvroRule {
std::vector<AvroMatchRule> matches;
std::string replace;
};
struct AvroPattern {
std::string find;
std::string replace;
std::vector<AvroRule> rules;
};
class AvroParser {
public:
AvroParser();
std::string parse(const std::string &input) const;
private:
std::vector<AvroPattern> patterns_;
std::string vowel_;
std::string consonant_;
std::string caseSensitive_;
static bool isExact(const std::string &needle, const std::string &haystack, int start,
int end, bool negate);
bool isVowel(char c) const;
bool isConsonant(char c) const;
bool isPunctuation(char c) const;
bool isCaseSensitive(char c) const;
std::string fixString(const std::string &input) const;
};
} // namespace fcitx
#endif // FCITX5_ANDROID_AVRO_PARSER_H

View File

@ -0,0 +1,738 @@
std::vector<AvroPattern> makePatterns() {
return {
AvroPattern{"bhl", "ভ্ল", {
}},
AvroPattern{"psh", "পশ", {
}},
AvroPattern{"bdh", "ব্ধ", {
}},
AvroPattern{"bj", "ব্জ", {
}},
AvroPattern{"bd", "ব্দ", {
}},
AvroPattern{"bb", "ব্ব", {
}},
AvroPattern{"bl", "ব্ল", {
}},
AvroPattern{"bh", "", {
}},
AvroPattern{"vl", "ভ্ল", {
}},
AvroPattern{"b", "", {
}},
AvroPattern{"v", "", {
}},
AvroPattern{"cNG", "চ্ঞ", {
}},
AvroPattern{"cch", "চ্ছ", {
}},
AvroPattern{"cc", "চ্চ", {
}},
AvroPattern{"ch", "", {
}},
AvroPattern{"c", "", {
}},
AvroPattern{"dhn", "ধ্ন", {
}},
AvroPattern{"dhm", "ধ্ম", {
}},
AvroPattern{"dgh", "দ্ঘ", {
}},
AvroPattern{"ddh", "দ্ধ", {
}},
AvroPattern{"dbh", "দ্ভ", {
}},
AvroPattern{"dv", "দ্ভ", {
}},
AvroPattern{"dm", "দ্ম", {
}},
AvroPattern{"DD", "ড্ড", {
}},
AvroPattern{"Dh", "", {
}},
AvroPattern{"dh", "", {
}},
AvroPattern{"dg", "দ্গ", {
}},
AvroPattern{"dd", "দ্দ", {
}},
AvroPattern{"D", "", {
}},
AvroPattern{"d", "", {
}},
AvroPattern{"...", "...", {
}},
AvroPattern{".`", ".", {
}},
AvroPattern{"..", "।।", {
}},
AvroPattern{".", "", {
}},
AvroPattern{"ghn", "ঘ্ন", {
}},
AvroPattern{"Ghn", "ঘ্ন", {
}},
AvroPattern{"gdh", "গ্ধ", {
}},
AvroPattern{"Gdh", "গ্ধ", {
}},
AvroPattern{"gN", "গ্ণ", {
}},
AvroPattern{"GN", "গ্ণ", {
}},
AvroPattern{"gn", "গ্ন", {
}},
AvroPattern{"Gn", "গ্ন", {
}},
AvroPattern{"gm", "গ্ম", {
}},
AvroPattern{"Gm", "গ্ম", {
}},
AvroPattern{"gl", "গ্ল", {
}},
AvroPattern{"Gl", "গ্ল", {
}},
AvroPattern{"gg", "জ্ঞ", {
}},
AvroPattern{"GG", "জ্ঞ", {
}},
AvroPattern{"Gg", "জ্ঞ", {
}},
AvroPattern{"gG", "জ্ঞ", {
}},
AvroPattern{"gh", "", {
}},
AvroPattern{"Gh", "", {
}},
AvroPattern{"g", "", {
}},
AvroPattern{"G", "", {
}},
AvroPattern{"hN", "হ্ণ", {
}},
AvroPattern{"hn", "হ্ন", {
}},
AvroPattern{"hm", "হ্ম", {
}},
AvroPattern{"hl", "হ্ল", {
}},
AvroPattern{"h", "", {
}},
AvroPattern{"jjh", "জ্ঝ", {
}},
AvroPattern{"jNG", "জ্ঞ", {
}},
AvroPattern{"jh", "", {
}},
AvroPattern{"jj", "জ্জ", {
}},
AvroPattern{"j", "", {
}},
AvroPattern{"J", "", {
}},
AvroPattern{"kkhN", "ক্ষ্ণ", {
}},
AvroPattern{"kShN", "ক্ষ্ণ", {
}},
AvroPattern{"kkhm", "ক্ষ্ম", {
}},
AvroPattern{"kShm", "ক্ষ্ম", {
}},
AvroPattern{"kxN", "ক্ষ্ণ", {
}},
AvroPattern{"kxm", "ক্ষ্ম", {
}},
AvroPattern{"kkh", "ক্ষ", {
}},
AvroPattern{"kSh", "ক্ষ", {
}},
AvroPattern{"ksh", "কশ", {
}},
AvroPattern{"kx", "ক্ষ", {
}},
AvroPattern{"kk", "ক্ক", {
}},
AvroPattern{"kT", "ক্ট", {
}},
AvroPattern{"kt", "ক্ত", {
}},
AvroPattern{"kl", "ক্ল", {
}},
AvroPattern{"ks", "ক্স", {
}},
AvroPattern{"kh", "", {
}},
AvroPattern{"k", "", {
}},
AvroPattern{"lbh", "ল্ভ", {
}},
AvroPattern{"ldh", "ল্ধ", {
}},
AvroPattern{"lkh", "লখ", {
}},
AvroPattern{"lgh", "লঘ", {
}},
AvroPattern{"lph", "লফ", {
}},
AvroPattern{"lk", "ল্ক", {
}},
AvroPattern{"lg", "ল্গ", {
}},
AvroPattern{"lT", "ল্ট", {
}},
AvroPattern{"lD", "ল্ড", {
}},
AvroPattern{"lp", "ল্প", {
}},
AvroPattern{"lv", "ল্ভ", {
}},
AvroPattern{"lm", "ল্ম", {
}},
AvroPattern{"ll", "ল্ল", {
}},
AvroPattern{"lb", "ল্ব", {
}},
AvroPattern{"l", "", {
}},
AvroPattern{"mth", "ম্থ", {
}},
AvroPattern{"mph", "ম্ফ", {
}},
AvroPattern{"mbh", "ম্ভ", {
}},
AvroPattern{"mpl", "মপ্ল", {
}},
AvroPattern{"mn", "ম্ন", {
}},
AvroPattern{"mp", "ম্প", {
}},
AvroPattern{"mv", "ম্ভ", {
}},
AvroPattern{"mm", "ম্ম", {
}},
AvroPattern{"ml", "ম্ল", {
}},
AvroPattern{"mb", "ম্ব", {
}},
AvroPattern{"mf", "ম্ফ", {
}},
AvroPattern{"m", "", {
}},
AvroPattern{"0", "", {
}},
AvroPattern{"1", "", {
}},
AvroPattern{"2", "", {
}},
AvroPattern{"3", "", {
}},
AvroPattern{"4", "", {
}},
AvroPattern{"5", "", {
}},
AvroPattern{"6", "", {
}},
AvroPattern{"7", "", {
}},
AvroPattern{"8", "", {
}},
AvroPattern{"9", "", {
}},
AvroPattern{"NgkSh", "ঙ্ক্ষ", {
}},
AvroPattern{"Ngkkh", "ঙ্ক্ষ", {
}},
AvroPattern{"NGch", "ঞ্ছ", {
}},
AvroPattern{"Nggh", "ঙ্ঘ", {
}},
AvroPattern{"Ngkh", "ঙ্খ", {
}},
AvroPattern{"NGjh", "ঞ্ঝ", {
}},
AvroPattern{"ngOU", "ঙ্গৌ", {
}},
AvroPattern{"ngOI", "ঙ্গৈ", {
}},
AvroPattern{"Ngkx", "ঙ্ক্ষ", {
}},
AvroPattern{"NGc", "ঞ্চ", {
}},
AvroPattern{"nch", "ঞ্ছ", {
}},
AvroPattern{"njh", "ঞ্ঝ", {
}},
AvroPattern{"ngh", "ঙ্ঘ", {
}},
AvroPattern{"Ngk", "ঙ্ক", {
}},
AvroPattern{"Ngx", "ঙ্ষ", {
}},
AvroPattern{"Ngg", "ঙ্গ", {
}},
AvroPattern{"Ngm", "ঙ্ম", {
}},
AvroPattern{"NGj", "ঞ্জ", {
}},
AvroPattern{"ndh", "ন্ধ", {
}},
AvroPattern{"nTh", "ন্ঠ", {
}},
AvroPattern{"NTh", "ণ্ঠ", {
}},
AvroPattern{"nth", "ন্থ", {
}},
AvroPattern{"nkh", "ঙ্খ", {
}},
AvroPattern{"ngo", "ঙ্গ", {
}},
AvroPattern{"nga", "ঙ্গা", {
}},
AvroPattern{"ngi", "ঙ্গি", {
}},
AvroPattern{"ngI", "ঙ্গী", {
}},
AvroPattern{"ngu", "ঙ্গু", {
}},
AvroPattern{"ngU", "ঙ্গূ", {
}},
AvroPattern{"nge", "ঙ্গে", {
}},
AvroPattern{"ngO", "ঙ্গো", {
}},
AvroPattern{"NDh", "ণ্ঢ", {
}},
AvroPattern{"nsh", "নশ", {
}},
AvroPattern{"Ngr", "ঙর", {
}},
AvroPattern{"NGr", "ঞর", {
}},
AvroPattern{"ngr", "ংর", {
}},
AvroPattern{"nj", "ঞ্জ", {
}},
AvroPattern{"Ng", "", {
}},
AvroPattern{"NG", "", {
}},
AvroPattern{"nk", "ঙ্ক", {
}},
AvroPattern{"ng", "", {
}},
AvroPattern{"nn", "ন্ন", {
}},
AvroPattern{"NN", "ণ্ণ", {
}},
AvroPattern{"Nn", "ণ্ন", {
}},
AvroPattern{"nm", "ন্ম", {
}},
AvroPattern{"Nm", "ণ্ম", {
}},
AvroPattern{"nd", "ন্দ", {
}},
AvroPattern{"nT", "ন্ট", {
}},
AvroPattern{"NT", "ণ্ট", {
}},
AvroPattern{"nD", "ন্ড", {
}},
AvroPattern{"ND", "ণ্ড", {
}},
AvroPattern{"nt", "ন্ত", {
}},
AvroPattern{"ns", "ন্স", {
}},
AvroPattern{"nc", "ঞ্চ", {
}},
AvroPattern{"n", "", {
}},
AvroPattern{"N", "", {
}},
AvroPattern{"OI`", "", {
}},
AvroPattern{"OU`", "", {
}},
AvroPattern{"O`", "", {
}},
AvroPattern{"OI", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, ""},
}},
AvroPattern{"OU", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, ""},
}},
AvroPattern{"O", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, ""},
}},
AvroPattern{"phl", "ফ্ল", {
}},
AvroPattern{"pT", "প্ট", {
}},
AvroPattern{"pt", "প্ত", {
}},
AvroPattern{"pn", "প্ন", {
}},
AvroPattern{"pp", "প্প", {
}},
AvroPattern{"pl", "প্ল", {
}},
AvroPattern{"ps", "প্স", {
}},
AvroPattern{"ph", "", {
}},
AvroPattern{"fl", "ফ্ল", {
}},
AvroPattern{"f", "", {
}},
AvroPattern{"p", "", {
}},
AvroPattern{"rri`", "", {
}},
AvroPattern{"rri", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, ""},
}},
AvroPattern{"rrZ", "রর‍্য", {
}},
AvroPattern{"rry", "রর‍্য", {
}},
AvroPattern{"rZ", "র‍্য", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", false},
AvroMatchRule{"prefix", "exact", "r", true},
AvroMatchRule{"prefix", "exact", "y", true},
AvroMatchRule{"prefix", "exact", "w", true},
AvroMatchRule{"prefix", "exact", "x", true},
}, "্র্য"},
}},
AvroPattern{"ry", "র‍্য", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", false},
AvroMatchRule{"prefix", "exact", "r", true},
AvroMatchRule{"prefix", "exact", "y", true},
AvroMatchRule{"prefix", "exact", "w", true},
AvroMatchRule{"prefix", "exact", "x", true},
}, "্র্য"},
}},
AvroPattern{"rr", "রর", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "vowel", "", true},
AvroMatchRule{"suffix", "exact", "r", true},
AvroMatchRule{"suffix", "punctuation", "", true},
}, "র্"},
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", false},
AvroMatchRule{"prefix", "exact", "r", true},
}, "্রর"},
}},
AvroPattern{"Rg", "ড়্গ", {
}},
AvroPattern{"Rh", "", {
}},
AvroPattern{"R", "", {
}},
AvroPattern{"r", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", false},
AvroMatchRule{"prefix", "exact", "r", true},
AvroMatchRule{"prefix", "exact", "y", true},
AvroMatchRule{"prefix", "exact", "w", true},
AvroMatchRule{"prefix", "exact", "x", true},
AvroMatchRule{"prefix", "exact", "Z", true},
}, "্র"},
}},
AvroPattern{"shch", "শ্ছ", {
}},
AvroPattern{"ShTh", "ষ্ঠ", {
}},
AvroPattern{"Shph", "ষ্ফ", {
}},
AvroPattern{"Sch", "শ্ছ", {
}},
AvroPattern{"skl", "স্ক্ল", {
}},
AvroPattern{"skh", "স্খ", {
}},
AvroPattern{"sth", "স্থ", {
}},
AvroPattern{"sph", "স্ফ", {
}},
AvroPattern{"shc", "শ্চ", {
}},
AvroPattern{"sht", "শ্ত", {
}},
AvroPattern{"shn", "শ্ন", {
}},
AvroPattern{"shm", "শ্ম", {
}},
AvroPattern{"shl", "শ্ল", {
}},
AvroPattern{"Shk", "ষ্ক", {
}},
AvroPattern{"ShT", "ষ্ট", {
}},
AvroPattern{"ShN", "ষ্ণ", {
}},
AvroPattern{"Shp", "ষ্প", {
}},
AvroPattern{"Shf", "ষ্ফ", {
}},
AvroPattern{"Shm", "ষ্ম", {
}},
AvroPattern{"spl", "স্প্ল", {
}},
AvroPattern{"sk", "স্ক", {
}},
AvroPattern{"Sc", "শ্চ", {
}},
AvroPattern{"sT", "স্ট", {
}},
AvroPattern{"st", "স্ত", {
}},
AvroPattern{"sn", "স্ন", {
}},
AvroPattern{"sp", "স্প", {
}},
AvroPattern{"sf", "স্ফ", {
}},
AvroPattern{"sm", "স্ম", {
}},
AvroPattern{"sl", "স্ল", {
}},
AvroPattern{"sh", "", {
}},
AvroPattern{"Sc", "শ্চ", {
}},
AvroPattern{"St", "শ্ত", {
}},
AvroPattern{"Sn", "শ্ন", {
}},
AvroPattern{"Sm", "শ্ম", {
}},
AvroPattern{"Sl", "শ্ল", {
}},
AvroPattern{"Sh", "", {
}},
AvroPattern{"s", "", {
}},
AvroPattern{"S", "", {
}},
AvroPattern{"oo`", "", {
}},
AvroPattern{"oo", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"o`", "", {
}},
AvroPattern{"oZ", "অ্য", {
}},
AvroPattern{"o", "", {
AvroRule{{
AvroMatchRule{"prefix", "vowel", "", false},
AvroMatchRule{"prefix", "exact", "o", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "vowel", "", false},
AvroMatchRule{"prefix", "exact", "o", false},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, ""},
}},
AvroPattern{"tth", "ত্থ", {
}},
AvroPattern{"t``", "", {
}},
AvroPattern{"TT", "ট্ট", {
}},
AvroPattern{"Tm", "ট্ম", {
}},
AvroPattern{"Th", "", {
}},
AvroPattern{"tn", "ত্ন", {
}},
AvroPattern{"tm", "ত্ম", {
}},
AvroPattern{"th", "", {
}},
AvroPattern{"tt", "ত্ত", {
}},
AvroPattern{"T", "", {
}},
AvroPattern{"t", "", {
}},
AvroPattern{"aZ", "অ্যা", {
}},
AvroPattern{"AZ", "অ্যা", {
}},
AvroPattern{"a`", "", {
}},
AvroPattern{"A`", "", {
}},
AvroPattern{"a", "", {
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"prefix", "exact", "a", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, "য়া"},
AvroRule{{
AvroMatchRule{"prefix", "exact", "a", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"i`", "ি", {
}},
AvroPattern{"i", "ি", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"I`", "", {
}},
AvroPattern{"I", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"u`", "", {
}},
AvroPattern{"u", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"U`", "", {
}},
AvroPattern{"U", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"ee`", "", {
}},
AvroPattern{"ee", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"e`", "", {
}},
AvroPattern{"e", "", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "exact", "`", true},
}, ""},
}},
AvroPattern{"z", "", {
}},
AvroPattern{"Z", "্য", {
}},
AvroPattern{"y", "্য", {
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", true},
AvroMatchRule{"prefix", "punctuation", "", true},
}, ""},
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, "ইয়"},
}},
AvroPattern{"Y", "", {
}},
AvroPattern{"q", "", {
}},
AvroPattern{"w", "", {
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
AvroMatchRule{"suffix", "vowel", "", false},
}, "ওয়"},
AvroRule{{
AvroMatchRule{"prefix", "consonant", "", false},
}, "্ব"},
}},
AvroPattern{"x", "ক্স", {
AvroRule{{
AvroMatchRule{"prefix", "punctuation", "", false},
}, "এক্স"},
}},
AvroPattern{":`", ":", {
}},
AvroPattern{":", "", {
}},
AvroPattern{"^`", "^", {
}},
AvroPattern{"^", "", {
}},
AvroPattern{",,", "্‌", {
}},
AvroPattern{",", ",", {
}},
AvroPattern{"$", "", {
}},
AvroPattern{"`", "", {
}},
};
}
std::string makeVowels() { return "aeiou"; }
std::string makeConsonants() { return "bcdfghjklmnpqrstvwxyz"; }
std::string makeCaseSensitive() { return "oiudgjnrstyz"; }

View File

@ -0,0 +1,7 @@
[InputMethod]
Name=Probhat
Icon=fcitx-avro
Label=Probhat
LangCode=bn
Addon=avro
Configurable=True

View File

@ -0,0 +1,28 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
package org.fcitx.fcitx5.android.plugin.avro
/** Shared identifiers for Bangla Avro / Probhat keyboard integration. */
object AvroKeyboardIds {
/** Fcitx input method unique name for Avro phonetic typing. */
const val IME_AVRO = "avro"
/** Fcitx input method unique name for Probhat fixed layout. */
const val IME_PROBHAT = "probhat"
/** Soft keyboard layout id (roman QWERTY feeding the Avro engine). */
const val LAYOUT_AVRO = "keyboard-bn-avro"
/** Soft keyboard layout id (Probhat Bengali fixed layout). */
const val LAYOUT_PROBHAT = "keyboard-bn-probhat"
fun isBanglaIme(uniqueName: String): Boolean =
uniqueName == IME_AVRO || uniqueName == IME_PROBHAT
fun layoutForIme(uniqueName: String): String? = when (uniqueName) {
IME_AVRO -> LAYOUT_AVRO
IME_PROBHAT -> LAYOUT_PROBHAT
else -> null
}
}

View File

@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M69,25 L134.34,90.35 115.73,131.73 39,55C49.93,44.18 58.13,35.88 69,25Z"
android:strokeLineJoin="miter"
android:strokeLineCap="butt">
<aapt:attr name="android:fillColor">
<gradient
android:startX="54"
android:startY="54"
android:endX="85"
android:endY="85"
android:type="linear">
<item android:offset="0" android:color="#44000000"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M40,26h28v28h-28z"
android:strokeWidth="2"
android:fillColor="#ffffff"
android:strokeColor="#000000"/>
<path
android:fillColor="#000000"
android:pathData="m53.13,46.15q-2.6,0 -4.54,-0.76 -1.93,-0.78 -3.02,-2.3 -1.06,-1.54 -1.06,-3.78 0,-1.88 0.81,-3.14 0.84,-1.26 2.18,-1.96 0.81,-0.42 1.71,-0.67 0.92,-0.25 2.16,-0.34 1.26,-0.11 3.02,-0.11h1.88q-0.08,-1.32 -0.84,-1.93 -0.73,-0.62 -1.88,-0.62 -1.09,0 -1.65,0.42 -0.56,0.39 -0.56,1.09 0,0.11 0,0.22 0.03,0.11 0.03,0.2l-1.99,0.25q-0.03,-0.25 -0.06,-0.48 0,-0.22 0,-0.39 0,-1.54 1.09,-2.35 1.09,-0.81 3,-0.81 2.91,0 4.03,2.35 0.76,-1.29 2.04,-2.38l1.54,1.23q-0.08,0.14 -0.14,0.25 -0.03,0.11 -0.03,0.34 0,0.31 0.2,0.7 0.2,0.39 0.73,1.23 0.5,0.76 0.81,1.46 0.31,0.67 0.31,1.43 0,1.2 -0.78,2.02 -0.78,0.81 -2.77,0.81 -0.53,0 -1.12,-0.14v5.66q0.5,-0.14 0.98,-0.36 0.5,-0.22 1.04,-0.5l0.73,1.88q-0.67,0.31 -1.37,0.56 -0.67,0.22 -1.37,0.39v5.88h-1.96v-5.54q-0.81,0.11 -1.62,0.14 -0.78,0.06 -1.51,0.06zM58.23,34.95v1.23q0.28,0.06 0.5,0.08 0.25,0.03 0.5,0.03 0.92,0 1.29,-0.31 0.36,-0.31 0.36,-0.9 0,-0.53 -0.22,-0.95 -0.2,-0.45 -0.59,-1.06 -0.31,-0.45 -0.5,-0.81 -0.17,-0.36 -0.25,-0.73 -0.7,0.84 -0.9,1.65 -0.2,0.81 -0.2,1.76zM53.35,44.16q0.76,0 1.48,-0.03 0.73,-0.03 1.43,-0.11v-9.07h-1.68q-1.96,0 -3.14,0.11 -1.15,0.11 -1.85,0.34 -0.7,0.22 -1.29,0.59 -1.68,1.04 -1.68,3.44 0,1.23 0.64,2.32 0.64,1.06 2.1,1.74 1.48,0.67 3.98,0.67z" />
</vector>

View File

@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="1.2"
android:scaleY="1.2"
android:translateX="-10.8"
android:translateY="-10.8">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="#696969"/>
<path
android:pathData="m40,69.8h28a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1H40a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m35.3,61.3h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m43.9,61.3h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m52.5,61.3h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m61.1,61.3h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m69.7,61.3h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m35.3,52.8h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m43.9,52.8h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m52.5,52.8h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m61.1,52.8h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
<path
android:pathData="m69.7,52.8h3a1,1 0,0 1,1 1v3a1,1 0,0 1,-1 1h-3a1,1 0,0 1,-1 -1v-3a1,1 0,0 1,1 -1z"
android:fillColor="#c5c5c5"/>
</group>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name_release">Fcitx5 (Avro Plugin)</string>
<string name="app_name_debug">Fcitx5 (Avro Plugin | Debug)</string>
<string name="description">Avro (Bangla phonetic input method) engine support for Fcitx5</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<plugin xmlns="../../../../../pluginSchema.xsd">
<apiVersion>0.1</apiVersion>
<domain>fcitx5-avro</domain>
<description>@string/description</description>
</plugin>

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
"""Generate avro_rules.inc from avro-phonetic.json for the native C++ parser."""
from __future__ import annotations
import json
import sys
from pathlib import Path
def esc(s: str) -> str:
return s.replace("\\", "\\\\").replace('"', '\\"')
def main() -> int:
plugin_root = Path(__file__).resolve().parents[1]
repo_root = plugin_root.parents[1]
json_path = repo_root.parent / "fcitx5" / "data" / "avro" / "avro-phonetic.json"
if len(sys.argv) > 1:
json_path = Path(sys.argv[1])
out_path = plugin_root / "src" / "main" / "cpp" / "avro_rules.inc"
if not json_path.is_file():
print(f"Rules JSON not found: {json_path}", file=sys.stderr)
return 1
data = json.loads(json_path.read_text(encoding="utf-8"))
lines: list[str] = []
lines.append("std::vector<AvroPattern> makePatterns() {")
lines.append(" return {")
for pattern in data["patterns"]:
lines.append(
f' AvroPattern{{"{esc(pattern["find"])}", "{esc(pattern["replace"])}", {{'
)
for rule in pattern.get("rules", []):
lines.append(" AvroRule{{")
for match in rule.get("matches", []):
scope = match.get("scope", "")
negative = bool(match.get("negative", False))
if scope.startswith("!"):
negative = True
scope = scope[1:]
neg = "true" if negative else "false"
lines.append(
f' AvroMatchRule{{"{esc(match.get("type", ""))}", '
f'"{esc(scope)}", "{esc(match.get("value", ""))}", {neg}}},'
)
lines.append(f' }}, "{esc(rule.get("replace", ""))}"}},')
lines.append(" }},")
lines.append(" };")
lines.append("}")
lines.append(f'std::string makeVowels() {{ return "{esc(data["vowel"])}"; }}')
lines.append(f'std::string makeConsonants() {{ return "{esc(data["consonant"])}"; }}')
lines.append(
f'std::string makeCaseSensitive() {{ return "{esc(data["casesensitive"])}"; }}'
)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Wrote {out_path} ({len(lines)} lines)")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,65 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "../src/main/cpp/avro_parser.h"
#include <cstdlib>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
namespace {
struct GoldenCase {
const char *input;
const char *expected;
};
int runGoldenTests() {
const std::vector<GoldenCase> cases = {
{"ami banglay gan gai", "আমি বাংলায় গান গাই"},
{"amader valObasa", "আমাদের ভালোবাসা"},
{"bhalo", "ভাল"},
{"bhalO", "ভালো"},
{"mukh", "মুখ"},
{"OI", ""},
{"OU", ""},
{"rry", "রর‍্য"},
{"t``", ""},
{",,", "্‌"},
{"x", "এক্স"},
{"kw", "ক্ব"},
{"ky", "ক্য"},
{"123", "১২৩"},
};
fcitx::AvroParser parser;
int failures = 0;
for (const auto &testCase : cases) {
const std::string input(testCase.input);
const std::string expected(testCase.expected);
const std::string actual = parser.parse(input);
if (actual != expected) {
++failures;
std::cerr << "FAIL: \"" << input << "\"\n"
<< " expected: " << expected << "\n"
<< " actual: " << actual << "\n";
} else {
std::cout << "PASS: \"" << input << "\" -> " << actual << "\n";
}
}
return failures;
}
} // namespace
int main() {
const int failures = runGoldenTests();
if (failures == 0) {
std::cout << "All golden tests passed.\n";
return EXIT_SUCCESS;
}
std::cerr << failures << " golden test(s) failed.\n";
return EXIT_FAILURE;
}

Binary file not shown.

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
"""Run Avro parser golden tests (Python reference + optional native host binary)."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
GOLDEN_CASES = [
("ami banglay gan gai", "আমি বাংলায় গান গাই"),
("amader valObasa", "আমাদের ভালোবাসা"),
("bhalo", "ভাল"), # lowercase trailing o is literal; use bhalO for o-kar
("bhalO", "ভালো"),
("mukh", "মুখ"),
("OI", ""),
("OU", ""),
("rry", "রর‍্য"),
("t``", ""),
(",,", "্‌"),
("x", "এক্স"),
("kw", "ক্ব"),
("ky", "ক্য"),
("123", "১২৩"),
]
def load_python_parser():
tools_dir = Path(__file__).resolve().parent
fcitx5_tools = tools_dir.parents[2].parent / "fcitx5" / "tools"
sys.path.insert(0, str(fcitx5_tools))
from avro_phonetic_parser import AvroPhoneticParser # noqa: PLC0415
json_path = fcitx5_tools.parent / "data" / "avro" / "avro-phonetic.json"
data = json.loads(json_path.read_text(encoding="utf-8"))
return AvroPhoneticParser(data)
def run_python_golden_tests() -> int:
parser = load_python_parser()
failures = 0
print("=== Python reference golden tests ===")
for input_text, expected in GOLDEN_CASES:
actual = parser.parse(input_text)
if actual != expected:
failures += 1
print(f'FAIL: "{input_text}"')
print(f" expected: {expected}")
print(f" actual: {actual}")
else:
print(f'PASS: "{input_text}" -> {actual}')
if failures:
print(f"{failures} Python golden test(s) failed.")
else:
print("All Python golden tests passed.")
return failures
def try_run_cpp_golden_tests() -> int | None:
tools_dir = Path(__file__).resolve().parent
cpp_dir = tools_dir.parent / "src" / "main" / "cpp"
source = tools_dir / "host_golden_test.cpp"
binary = tools_dir / "host_golden_test"
compilers = [
["g++", "-std=c++17", "-I", str(cpp_dir), str(source), str(cpp_dir / "avro_parser.cpp"), "-o", str(binary)],
["clang++", "-std=c++17", "-I", str(cpp_dir), str(source), str(cpp_dir / "avro_parser.cpp"), "-o", str(binary)],
]
for cmd in compilers:
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f"\n=== Native C++ golden tests ({cmd[0]}) ===")
result = subprocess.run([str(binary)], check=False, text=True, capture_output=True)
print(result.stdout, end="")
if result.stderr:
print(result.stderr, end="", file=sys.stderr)
return result.returncode
except FileNotFoundError:
continue
except subprocess.CalledProcessError as exc:
print(f"Compile failed with {cmd[0]}:", exc.stderr or exc.stdout, file=sys.stderr)
continue
print("\nSkipping native C++ golden tests (no g++/clang++ found).")
return None
def main() -> int:
failures = run_python_golden_tests()
cpp_result = try_run_cpp_golden_tests()
if cpp_result is not None:
failures += cpp_result
return failures
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -35,3 +35,4 @@ include(":plugin:chewing")
include(":plugin:sayura")
include(":plugin:jyutping")
include(":plugin:thai")
include(":plugin:avro")