diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 53f1eb8c..ba946f75 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 26dcded0..27441eb9 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -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(), Essentia private val keyboards: HashMap 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(), 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(), 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) } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/BanglaAvroKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/BanglaAvroKeyboard.kt new file mode 100644 index 00000000..6f198fb1 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/BanglaAvroKeyboard.kt @@ -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> = 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 { _, v -> + updateLangSwitchKey(v) + } + + private val keepLettersUppercase by AppPrefs.getInstance().keyboard.keepLettersUppercase + + init { + updateLangSwitchKey(showLangSwitchKey.getValue()) + showLangSwitchKey.registerOnChangeListener(showLangSwitchKeyListener) + } + + private val textKeys: List by lazy { + allViews.filterIsInstance().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 = 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) { + 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) + } + } + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/BengaliCommitKey.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/BengaliCommitKey.kt new file mode 100644 index 00000000..540d520c --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/BengaliCommitKey.kt @@ -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)) + ), +) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/ProbhatKeyboard.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/ProbhatKeyboard.kt new file mode 100644 index 00000000..85403eb0 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/bn/ProbhatKeyboard.kt @@ -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> = 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 { _, 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) = 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 + } +} diff --git a/plugin/avro/build.gradle.kts b/plugin/avro/build.gradle.kts new file mode 100644 index 00000000..54c73571 --- /dev/null +++ b/plugin/avro/build.gradle.kts @@ -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")) +} diff --git a/plugin/avro/org.fcitx.fcitx5.android.yml b/plugin/avro/org.fcitx.fcitx5.android.yml new file mode 100644 index 00000000..19c0f68d --- /dev/null +++ b/plugin/avro/org.fcitx.fcitx5.android.yml @@ -0,0 +1,4 @@ +name: Avro Plugin +components: + - name: plugin + type: plugin diff --git a/plugin/avro/proguard-rules.pro b/plugin/avro/proguard-rules.pro new file mode 100644 index 00000000..f2605de5 --- /dev/null +++ b/plugin/avro/proguard-rules.pro @@ -0,0 +1 @@ +-keep class org.fcitx.fcitx5.android.plugin.avro.** { *; } diff --git a/plugin/avro/src/main/AndroidManifest.xml b/plugin/avro/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d750cdc1 --- /dev/null +++ b/plugin/avro/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/plugin/avro/src/main/assets/fcitx5/libavro.so b/plugin/avro/src/main/assets/fcitx5/libavro.so new file mode 100644 index 00000000..3945031b Binary files /dev/null and b/plugin/avro/src/main/assets/fcitx5/libavro.so differ diff --git a/plugin/avro/src/main/cpp/CMakeLists.txt b/plugin/avro/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..9c93ef3b --- /dev/null +++ b/plugin/avro/src/main/cpp/CMakeLists.txt @@ -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) diff --git a/plugin/avro/src/main/cpp/avro-addon.conf.in.in b/plugin/avro/src/main/cpp/avro-addon.conf.in.in new file mode 100644 index 00000000..de369c68 --- /dev/null +++ b/plugin/avro/src/main/cpp/avro-addon.conf.in.in @@ -0,0 +1,7 @@ +[Addon] +Name=Avro Bangla Phonetic +Category=InputMethod +Library=avro +Type=SharedLibrary +OnDemand=True +Configurable=True diff --git a/plugin/avro/src/main/cpp/avro.conf.in b/plugin/avro/src/main/cpp/avro.conf.in new file mode 100644 index 00000000..1e52a8ff --- /dev/null +++ b/plugin/avro/src/main/cpp/avro.conf.in @@ -0,0 +1,7 @@ +[InputMethod] +Name=Avro +Icon=fcitx-avro +Label=Avro +LangCode=bn +Addon=avro +Configurable=True diff --git a/plugin/avro/src/main/cpp/avro.cpp b/plugin/avro/src/main/cpp/avro.cpp new file mode 100644 index 00000000..71f71e19 --- /dev/null +++ b/plugin/avro/src/main/cpp/avro.cpp @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#include "avro.h" + +#include +#include +#include +#include + +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(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 diff --git a/plugin/avro/src/main/cpp/avro.h b/plugin/avro/src/main/cpp/avro.h new file mode 100644 index 00000000..5405024f --- /dev/null +++ b/plugin/avro/src/main/cpp/avro.h @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#ifndef FCITX_AVRO_AVRO_H +#define FCITX_AVRO_AVRO_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 diff --git a/plugin/avro/src/main/cpp/avro_parser.cpp b/plugin/avro/src/main/cpp/avro_parser.cpp new file mode 100644 index 00000000..2a074d46 --- /dev/null +++ b/plugin/avro/src/main/cpp/avro_parser.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#include "avro_parser.h" + +#include +#include + +namespace fcitx { + +namespace { +std::vector 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(haystack.size()) && + haystack.substr(start, end - start) == needle; + return equal ^ negate; +} + +bool AvroParser::isVowel(char c) const { + const auto lower = static_cast(std::tolower(static_cast(c))); + return vowel_.find(lower) != std::string::npos; +} + +bool AvroParser::isConsonant(char c) const { + const auto lower = static_cast(std::tolower(static_cast(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(std::tolower(static_cast(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(std::tolower(static_cast(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(fixed.size())) { + const int start = cur; + bool matched = false; + + for (const auto &pattern : patterns_) { + const int end = cur + static_cast(pattern.find.size()); + if (end > static_cast(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(fixed.size()) && m.type == "suffix") || + (chk >= 0 && chk < static_cast(fixed.size()) && + isPunctuation(fixed[chk])); + } else if (m.scope == "vowel") { + const bool inRange = (chk >= 0 && m.type == "prefix") || + (chk < static_cast(fixed.size()) && + m.type == "suffix"); + cond = inRange && chk >= 0 && chk < static_cast(fixed.size()) && + isVowel(fixed[chk]); + } else if (m.scope == "consonant") { + const bool inRange = (chk >= 0 && m.type == "prefix") || + (chk < static_cast(fixed.size()) && + m.type == "suffix"); + cond = inRange && chk >= 0 && chk < static_cast(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(m.value.size()); + } else { + s = start - static_cast(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 diff --git a/plugin/avro/src/main/cpp/avro_parser.h b/plugin/avro/src/main/cpp/avro_parser.h new file mode 100644 index 00000000..b0f65b8b --- /dev/null +++ b/plugin/avro/src/main/cpp/avro_parser.h @@ -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 +#include + +namespace fcitx { + +struct AvroMatchRule { + std::string type; + std::string scope; + std::string value; + bool negative; +}; + +struct AvroRule { + std::vector matches; + std::string replace; +}; + +struct AvroPattern { + std::string find; + std::string replace; + std::vector rules; +}; + +class AvroParser { +public: + AvroParser(); + std::string parse(const std::string &input) const; + +private: + std::vector 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 diff --git a/plugin/avro/src/main/cpp/avro_rules.inc b/plugin/avro/src/main/cpp/avro_rules.inc new file mode 100644 index 00000000..e8f2eb01 --- /dev/null +++ b/plugin/avro/src/main/cpp/avro_rules.inc @@ -0,0 +1,738 @@ +std::vector 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"; } diff --git a/plugin/avro/src/main/cpp/probhat.conf.in b/plugin/avro/src/main/cpp/probhat.conf.in new file mode 100644 index 00000000..d49492bb --- /dev/null +++ b/plugin/avro/src/main/cpp/probhat.conf.in @@ -0,0 +1,7 @@ +[InputMethod] +Name=Probhat +Icon=fcitx-avro +Label=Probhat +LangCode=bn +Addon=avro +Configurable=True diff --git a/plugin/avro/src/main/java/org/fcitx/fcitx5/android/plugin/avro/AvroKeyboardIds.kt b/plugin/avro/src/main/java/org/fcitx/fcitx5/android/plugin/avro/AvroKeyboardIds.kt new file mode 100644 index 00000000..fb6bfdb4 --- /dev/null +++ b/plugin/avro/src/main/java/org/fcitx/fcitx5/android/plugin/avro/AvroKeyboardIds.kt @@ -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 + } +} diff --git a/plugin/avro/src/main/res/drawable-v24/ic_launcher_foreground.xml b/plugin/avro/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..5e4fdff3 --- /dev/null +++ b/plugin/avro/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/plugin/avro/src/main/res/drawable/ic_launcher_background.xml b/plugin/avro/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..385ec3f1 --- /dev/null +++ b/plugin/avro/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/plugin/avro/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/plugin/avro/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..bbd3e021 --- /dev/null +++ b/plugin/avro/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/plugin/avro/src/main/res/values/strings.xml b/plugin/avro/src/main/res/values/strings.xml new file mode 100644 index 00000000..5c379195 --- /dev/null +++ b/plugin/avro/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Fcitx5 (Avro Plugin) + Fcitx5 (Avro Plugin | Debug) + Avro (Bangla phonetic input method) engine support for Fcitx5 + diff --git a/plugin/avro/src/main/res/xml/plugin.xml b/plugin/avro/src/main/res/xml/plugin.xml new file mode 100644 index 00000000..f56f68ee --- /dev/null +++ b/plugin/avro/src/main/res/xml/plugin.xml @@ -0,0 +1,6 @@ + + + 0.1 + fcitx5-avro + @string/description + diff --git a/plugin/avro/tools/gen_avro_rules_inc.py b/plugin/avro/tools/gen_avro_rules_inc.py new file mode 100644 index 00000000..69757b20 --- /dev/null +++ b/plugin/avro/tools/gen_avro_rules_inc.py @@ -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 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()) diff --git a/plugin/avro/tools/host_golden_test.cpp b/plugin/avro/tools/host_golden_test.cpp new file mode 100644 index 00000000..76b7c9f0 --- /dev/null +++ b/plugin/avro/tools/host_golden_test.cpp @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#include "../src/main/cpp/avro_parser.h" + +#include +#include +#include +#include +#include + +namespace { + +struct GoldenCase { + const char *input; + const char *expected; +}; + +int runGoldenTests() { + const std::vector 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; +} diff --git a/plugin/avro/tools/host_golden_test.exe b/plugin/avro/tools/host_golden_test.exe new file mode 100644 index 00000000..776f303e Binary files /dev/null and b/plugin/avro/tools/host_golden_test.exe differ diff --git a/plugin/avro/tools/run_golden_tests.py b/plugin/avro/tools/run_golden_tests.py new file mode 100644 index 00000000..63d3961e --- /dev/null +++ b/plugin/avro/tools/run_golden_tests.py @@ -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()) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7744dd60..cf799226 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,3 +35,4 @@ include(":plugin:chewing") include(":plugin:sayura") include(":plugin:jyutping") include(":plugin:thai") +include(":plugin:avro")