From 5b2f6e6e5960dd8dc054d65170ca3dd9d745635b Mon Sep 17 00:00:00 2001 From: will wade Date: Fri, 30 Oct 2026 05:25:18 +0000 Subject: [PATCH] =?UTF-8?q?feat(ime):=20compact=20live=20WPM=20in=20the=20?= =?UTF-8?q?keyboard's=20top=20bar=20=E2=80=94=20RFC=200012=20direct-entry?= =?UTF-8?q?=20clause=20(2026-10-30=20amendment)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 0012 exempted IME surfaces ('no persistent strip') — the Android IME now has a stable top bar, so the exemption is lifted by amendment (governance PR follows). Windows shows the same thing in its Direct Mode mini-bar. - Settings → Output → 'Show typing rate' (default OFF per the RFC): one toggle now governs BOTH surfaces — the main app's status-bar 'X.X cps · N wpm' readout (previously always-on; now opt-in as the RFC always specified) and the IME's compact 'N wpm' (current rate only, per the direct-entry clause — no cps/max/avg in the strip). - The IME readout refreshes at 2 Hz from the engine's rolling window (dasher_get_wpm) and re-reads the setting every tick — toggling takes effect without restarting the keyboard. Verified on-emulator: text present with the toggle on (x556-652 in the top bar), absent with it off (242-pixel diff, on-side only). - Telemetry: setting_changed fires on toggle (RFC 0001 amendment). GTK: its Settings → Output readout exists (2 Hz) but the keyboard-mode surface shows nothing — separate issue filed there. Signed-off-by: will wade --- .../at/dasher/android/DasherImeService.kt | 39 +++++++++++++++++ .../java/at/dasher/android/MainActivity.kt | 10 ++++- .../java/at/dasher/android/SettingsScreen.kt | 43 +++++++++++++++++++ .../at/dasher/android/TypingRateSetting.kt | 31 +++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/at/dasher/android/TypingRateSetting.kt diff --git a/app/src/main/java/at/dasher/android/DasherImeService.kt b/app/src/main/java/at/dasher/android/DasherImeService.kt index 11b513c..67e6f9b 100644 --- a/app/src/main/java/at/dasher/android/DasherImeService.kt +++ b/app/src/main/java/at/dasher/android/DasherImeService.kt @@ -51,6 +51,27 @@ class DasherImeService : InputMethodService() { private var floating = false private var floatingView: LinearLayout? = null private var floatDockBtn: android.widget.ImageButton? = null + + /** + * RFC 0012 direct-entry clause (2026-10-30 amendment): compact live WPM + * in the top bar. Refreshed at 2 Hz — the visibility is re-read each + * tick so toggling the setting takes effect without restarting the IME. + */ + private var wpmView: android.widget.TextView? = null + private val wpmTicker = object : Runnable { + override fun run() { + val view = wpmView ?: return + val eng = engine + if (eng == null || !TypingRateSetting.isEnabled(this@DasherImeService)) { + view.visibility = View.GONE + } else { + view.text = "${eng.wpm().toInt()} wpm" + view.visibility = View.VISIBLE + } + handler.postDelayed(this, 500) + } + } + private val handler = Handler(Looper.getMainLooper()) private var emojiToggleBtn: Button? = null // Last-used mode (dock/float), restored on the next keyboard show so @@ -210,6 +231,18 @@ class DasherImeService : InputMethodService() { gravity = Gravity.CENTER_VERTICAL or Gravity.END setPadding(dp(8, density), dp(2, density), dp(8, density), dp(2, density)) } + // RFC 0012 (2026-10-30 amendment) direct-entry clause: compact live + // WPM in the IME's control strip, current rate only, ~2 Hz, visible + // only when Settings → Output → Show typing rate is on. The original + // exemption ("no persistent strip") predates the stable top bar. + wpmView = android.widget.TextView(this).apply { + textSize = 13f + setTextColor(topBarTint()) + visibility = View.GONE + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { + marginEnd = dp(6, density) + } + } val floatBtn = iconButton(R.drawable.ic_lucide_picture_in_picture_2, "Float the keyboard") { enterFloatingMode() } // Emoji toggle (#61 option 2): GONE until createEngine() confirms // the data bundle ships the Emoji alphabet (DasherCore b00a4c8a+). @@ -234,6 +267,7 @@ class DasherImeService : InputMethodService() { } emojiToggleBtn = emojiBtn val hideBtn = iconButton(R.drawable.ic_lucide_keyboard_off, "Hide the keyboard") { requestHideSelf(0) } + top.addView(wpmView) top.addView(floatBtn) top.addView(emojiBtn) top.addView(hideBtn) @@ -866,6 +900,9 @@ class DasherImeService : InputMethodService() { override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { super.onStartInputView(info, restarting) Log.d(TAG, "onStartInputView(restarting=$restarting, floating=$floating)") + // RFC 0012: start the compact-WPM ticker with the keyboard. + handler.removeCallbacks(wpmTicker) + handler.postDelayed(wpmTicker, 500) // #78 review I2 — defense-in-depth for the LayoutParams stamp: the // framework replaces our docked-root LayoutParams on attach; the // posted-handler re-apply covers the attach, this covers any later @@ -888,6 +925,8 @@ class DasherImeService : InputMethodService() { override fun onFinishInputView(finishingInput: Boolean) { super.onFinishInputView(finishingInput) + handler.removeCallbacks(wpmTicker) + wpmView?.visibility = View.GONE engine?.stop() } diff --git a/app/src/main/java/at/dasher/android/MainActivity.kt b/app/src/main/java/at/dasher/android/MainActivity.kt index d3b567e..75f14c5 100644 --- a/app/src/main/java/at/dasher/android/MainActivity.kt +++ b/app/src/main/java/at/dasher/android/MainActivity.kt @@ -303,8 +303,16 @@ class MainActivity : ComponentActivity() { // RFC 0012: typing-rate overlay — CPS + WPM from the engine's rolling // window (dasher_get_cps / dasher_get_wpm), refreshed once a second // while playing. Apple shows the same "X.X cps · N wpm" readout. + // Opt-in via Settings → Output (2026-10-30: was always-on; the + // RFC's toggle now governs this AND the IME's compact WPM). var typingRate by remember { mutableStateOf("") } - LaunchedEffect(isPlaying) { + var typingRateEnabled by remember { mutableStateOf(TypingRateSetting.isEnabled(applicationContext)) } + val appCtx = applicationContext + LaunchedEffect(showSettings) { // re-read when Settings closes + typingRateEnabled = TypingRateSetting.isEnabled(appCtx) + } + LaunchedEffect(isPlaying, typingRateEnabled) { + if (!typingRateEnabled) { typingRate = ""; return@LaunchedEffect } while (isPlaying) { engine?.let { e -> typingRate = getString(R.string.typing_rate, e.cps(), e.wpm().toInt()) diff --git a/app/src/main/java/at/dasher/android/SettingsScreen.kt b/app/src/main/java/at/dasher/android/SettingsScreen.kt index b572daa..0b19c73 100644 --- a/app/src/main/java/at/dasher/android/SettingsScreen.kt +++ b/app/src/main/java/at/dasher/android/SettingsScreen.kt @@ -166,6 +166,11 @@ fun SettingsScreen( // keyboard from the shared dasher_ime prefs. item { KeyboardStartSection() } } + if (tabGroup == "Output") { + // RFC 0012 (2026-10-30 amendment): the opt-in toggle — + // gates the main-app readout AND the IME's compact WPM. + item { TypingRateSection() } + } if (tabGroup == "Customization") { item { AppearanceSection(engine, bump) } // Output-pane font lives with the other appearance @@ -251,6 +256,44 @@ private fun PrivacyContent(context: android.content.Context) { * compatibility filter. */ +/** + * RFC 0012: the typing-rate opt-in. One toggle governs both surfaces (the + * main status-bar readout and the IME top bar's compact live-WPM — the + * 2026-10-30 amendment). Default OFF per the RFC. + */ +@Composable +private fun TypingRateSection() { + val context = androidx.compose.ui.platform.LocalContext.current + var enabled by remember { mutableStateOf(TypingRateSetting.isEnabled(context)) } + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = enabled, + role = Role.Switch, + onValueChange = { v -> + TypingRateSetting.setEnabled(context, v) + enabled = v + UiTelemetry.setting("Show typing rate", v, "settings") + }, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Show typing rate", style = MaterialTheme.typography.bodyLarge) + Text( + "Live characters-per-second and words-per-minute while writing — in the status bar here, and a compact words-per-minute figure on the keyboard's top bar.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch(checked = enabled, onCheckedChange = null) + } + } +} + /** * #75: IME start behaviour. The keyboard normally reopens in its last-used * mode (docked or floating); users who want it docked EVERY time — diff --git a/app/src/main/java/at/dasher/android/TypingRateSetting.kt b/app/src/main/java/at/dasher/android/TypingRateSetting.kt new file mode 100644 index 0000000..15957bb --- /dev/null +++ b/app/src/main/java/at/dasher/android/TypingRateSetting.kt @@ -0,0 +1,31 @@ +package at.dasher.android + +import android.content.Context + +/** + * RFC 0012 typing-rate display toggle. Frontend setting (DasherCore has no + * BP_SHOW_TYPING_RATE — Windows/Apple also implement it locally). Default + * OFF per the RFC ("doesn't clutter the main writing view"). + * + * Governs both surfaces that show a rate: + * - the main app's status-bar readout ("X.X cps · N wpm") + * - the IME top bar's compact live-WPM (RFC 0012 direct-entry clause, added + * 2026-10-30 amendment — the "no persistent strip" exemption predates the + * IME's stable top bar) + * + * Stored in the app's default SharedPreferences so the main activity and the + * IME service (same package, same process) share one value. + */ +object TypingRateSetting { + private const val PREFS = "at.dasher.android_preferences" + private const val KEY = "show_typing_rate" + + fun isEnabled(context: Context): Boolean = + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getBoolean(KEY, false) + + fun setEnabled(context: Context, value: Boolean) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit().putBoolean(KEY, value).apply() + } +}