A clone of the Light Phone 3's built-in keyboard, for any app — with swipe typing and a bundled-dictionary autocorrect.
⬇ Download the latest APK · free, open source.
Scan the code above with BrightMarket installed to open BrightKeyboard there and install or update it directly. Don't have BrightMarket yet? Get it, and browse every Bright app, at brightmarket.gzl.dev.
A fork of adam-weber/light-keyboard. The keyboard looks exactly the same; typing and autocorrect underneath it are new.
Current release: v4.6.x (tag v4.6.<n>). applicationId is com.gios.brightkeyboard.
It was app.lightphonekeyboard up to v3.11, which is the id of adam-weber/light-keyboard, the project this is a fork of. Keeping it stopped him shipping his own keyboard to a phone that had this one, so it went back. v3.11 hands its settings, saved words and learned touch model to v4.0 on first launch — see Migration.kt.
On a stock Light Phone, the black-and-white keyboard lives only inside Light's own tools — every other app gets the ordinary Android system keyboard, and there is no Light one to choose. This is a faithful recreation, packaged as a real system keyboard you can set as the default, so every app on a modified Light Phone shares the same look.
Upstream's autocorrect called Android's SpellCheckerSession. That does nothing on LightOS, because
there is no Google spell-check service installed to answer it — corrections silently never arrive.
So this fork bundles its own dictionary (res/raw/words.bin, ~63k words, generated by
tools/gen_dict.py) and both autocorrect and swipe typing run against it,
entirely on-device.
One thing here uses the network, and only one: the GIF button. Everything that types — the dictionary, autocorrect, the swipe model, the clipboard, the emoji panel — runs on the phone and always will. GIF search asks KLIPY; nothing else does, and nothing about what you type is ever sent anywhere.
A word list on its own only knows how common a word is, which is why a keyboard built on one suggests
things nobody would write: "happy bir" offers birth, "went wro" offers wrote, "how mu" offers
music. Since v1.0.9 there is a second bundled file, res/raw/bigrams.bin — 180,000 word pairs with
how much more often each pair occurs than chance would explain — and the suggestion strip, autocorrect
and swipe typing all use it to reorder what they were already going to offer. It is a lookup table, not
a language model: it sees one word back, it can only move a candidate a few places, and it can never
remove the word your spelling most obviously matches.
With Obtainium (recommended — keeps you updated):
- Install Obtainium.
- Add an app, and give it this repository:
https://github.com/gi-os/BrightKeyboard - It installs the latest release and tells you when a new one ships.
Or by hand: grab the APK from Releases and open it.
Turn it on — Bright Keyboard's own setup screen has both steps:
- Enable Bright Keyboard — opens Android's keyboard settings, where you switch it on.
- Choose it as your keyboard — makes it the active input method.
That's it — under five minutes, most of it Android's own settings UI.
Optional settings, all in the app itself:
- Autocorrect (on by default) — fixes misspellings when you finish a word. Four engines run at
once and their answers merge into one ranked list: a keyboard-aware edit distance, a sound-alike
index (
nitetonight), a missed-space search (alottoa lot), and a table of English facts (donttodon't). Off types exactly what you tap. - Autocorrect settings — two choices on their own page. How much it fixes (Cautious / Balanced / Eager) decides what the keyboard commits on its own. It never changes what the delete key offers, so turning it down makes the keyboard quieter rather than less capable. The delete key chooses between Show other words, which walks every guess and ends at exactly what you typed, and Undo the fix, which puts your spelling back in one press and stops.
- How it reads a swipe — Model (the default) or Shape matching. The model is a 635k-parameter neural network bundled in the APK and run on the phone; the shape matcher compares your trace against the path through each word's keys, which is what this keyboard did before and what most gesture keyboards still do. On 517 real human swipes, with this app's own dictionary behind both: 74.7% right first time for the shape matcher, 92.5% for the model. The shape matcher is always loaded and answers whenever the model can't — see Swipe typing below.
- Swipe typing (on by default) — drag from letter to letter to write a whole word, then lift. Guessed wrong? Delete walks the other words your trace could have meant. This works on the keypad too. The trail follows every touch position the screen reports, not one per frame, and tapers from thin at the tail to full width under your finger.
- Swipe settings — How far it reaches (Cautious / Balanced / Eager) decides what the decoder does when nothing matches: settle for the nearest word, or give you nothing. Words delete offers sets how many readings of one trace you can walk through, from two to eight.
- Suggestions (off by default) — a thin strip above the keys showing three words: completions of what you're typing, the correction autocorrect has in mind, or alternate swipe readings, ranked against the word before them. Tap one to use it. Hold one to forget it — the keyboard stops offering that word, from the bundled dictionary as well as your own list. The leftmost slot is the keep-as-typed slot (iOS-style): shown in quotes, tapping it commits your literal spelling and learns the word. A one-time login code lands in the first slot, pinned there from BrightChat for the three minutes it is worth anything, and only while nothing has been typed — the moment a field is focused and empty, which is when the code is wanted and the one moment the strip has nothing better to say. Tapping it commits the code exactly: no case coercion, no trailing space, and nothing learned into your dictionary. On a phone without BrightChat there is simply never one. It shows up whenever the word can't grow any further — including the moment after you finish an unknown word, and after autocorrect has replaced one, where tapping it puts your spelling back. Off by default because the LightOS keyboard has no suggestion bar of its own, and turning this on makes the keyboard one strip taller everywhere.
- My words — names and anything else the dictionary has never seen. Adding a word stops it being autocorrected into something else, makes it something autocorrect can arrive at, and makes it traceable by swipe. Without it, "Bjorn" becomes "born" every time. Anything you've forgotten from the suggestion strip is listed under the same screen, with a ✕ to put it back.
- Auto-Capitalize / Auto-Period (both on by default) — capitalize sentence starts; double-tap space for a period.
- Return key / Tools key (both on by default) — show or hide those keys. The tools key is where the emoji key used to be, and it opens the tools page described below.
- Hide-keyboard key (off by default) — a key in the bottom row that closes the keyboard without leaving the field. Off by default because the back gesture already does this, and the bottom row is narrow. Turn it on if that is the key your thumb reaches for.
- The tools page — the tools key opens a page of six tiles: Clipboard, Emoji, Faces, GIFs, One-handed and Hide keyboard. Six things that can only be done from here, and nothing that is a shortcut to somewhere else.
- GIFs — a full-screen page, not a keyboard-sized slot: they move, they are shown whole rather than cropped square, and a hold stars one. The star key shows only the starred ones, which is the one view here that needs no network at all. Search KLIPY and drop one straight into the field as a picture. Where a field takes no images — most fields are for text, and a keyboard cannot make one accept something it hasn't declared — the GIF goes on the clipboard and the keyboard presses paste for you, so it stays a picture either way. The ones you have used lately come first. This is the only part of the keyboard that uses the network — see Settings → GIFs, which says so, and where you can put your own KLIPY key if the shared one is busy.
- Searching a panel — the emoji, faces and GIF search keys borrow the letters, and return runs the search. Letters, digits and spaces build the query, backspace edits it, and it shows in the strip above the keys rather than in what you are writing. Nothing is searched until you ask.
- Clipboard history (on by default) — the last two dozen things you copied, newest first, three to a page. Tap one to paste it. Tap the pin beside it to keep it; pinned clips survive Clear and never fall off the end. A keyboard is the one kind of app Android lets read the clipboard while another app is in front, which is what makes this possible at all. It stays on the phone: nothing in the clipboard touches the network. A clip the copying app marked sensitive, which is what a password manager does, is never recorded. Turning the setting off clears what is already stored.
- One-handed — crowd the keys against the left or right edge so a thumb can reach all of them. The strip this frees carries one button that puts the keyboard back to full width, so it can always be undone without finding the setting. The tools page toggles it; the settings page chooses the side.
- Emoji — every emoji, not a fixed 24. The panel scrolls, with a jump per category and the ones
you used lately at the top, and anything this phone's font cannot draw is left out rather than shown
as a box. Press the magnifier to search by name:
pizzafinds 🍕 and so doeshungry. Pick a default skin tone once and the whole panel uses it; hold any emoji for its other tones and its gendered forms. Emoji in suggestions (off by default) puts an emoji in the suggestion strip when you type the name of a thing. - Faces — kaomoji, behind the Faces tile. 126 of them across seven categories, six to a
page, with the ones you used lately at the front. Tap one and it goes in; the page stays open,
because faces come in twos and threes. The magnifier searches by name —
shrug,table,bear— which is the only way to search a thing that has no name of its own. Add your own under Settings → Text faces: they get a category at the end of the page and come first in search. Anything this phone's font cannot draw is left out rather than shown as a row of boxes. - Key vibration (on by default) — the short tick under each key press. Android's own touch-vibration setting still sits above this one, so turning it on cannot override a phone with haptics switched off.
- Voice dictation (off by default) — downloads a ~40 MB offline speech-to-text model (Vosk) once, then a mic key lets you speak instead of type, on-device. A Delete model link appears once downloaded, to reclaim the space.
- Compact keyboard (off by default) — shorter keys, tighter spacing.
- Keyboard layout — QWERTY, AZERTY, QWERTZ, or Keypad (T9). The keypad puts three letters on a key and works the word out from the digits, correcting a wrong key as it goes. Pick Multi-tap under it for the old press-2-three-times-for-C behavior, with no prediction at all.
- The globe key — appears on the bottom row on its own, whenever the phone has more than one keyboard enabled. Tap it to move to the next one.
Layout and appearance changes take effect the next time the keyboard opens.
The keyboard draws the keys on a fixed grid and never moves them. What moves is the target behind each one.
Nobody hits the middle of a key. A finger lands below where it aimed, because you aim with the tip and the screen senses the pad. Holz and Baudisch named the gap between the two the perceived input point. How far below is personal, differs from key to key, and holds steady for one person.
The keyboard measures yours. For each of the 26 letters it keeps two numbers: where your taps for that key center, and how far they scatter. An unclear tap is read against those, not against the drawn rectangle. Nothing on screen changes.
Three rules keep it from becoming a keyboard that argues with you.
A tap in the core of a key types that key. The middle 72% of every key's area is anchored. Nothing overrules it, not the spelling model and not anything learned here. Gunawardana, Paek and Meek showed in 2010 what happens without such a floor: the model overrules a deliberate, well-aimed tap. People mind that one error far more than they thank you for the ones it prevents. The drawn key is a promise.
It learns only from taps you let stand. A letter you delete straight away teaches it nothing. That is the one honest signal a keyboard has about what you meant. It is also what stops the thing feeding on itself. The correction that would send it off course is the same event that drops the tap from the sample.
It moves slowly and not far. About thirty taps to follow a change. A key's center may travel a third of a key and no further. Past that the keyboard would be guessing at your intent rather than correcting for how a fingertip is sensed, which is the only job it has.
Space, return, delete, shift and 123 are in it too, on the same terms. Their sideways offset is measured against their own width rather than a letter's, since a space bar is five cells wide.
The model is 31 numbers a side. It lives in this app's own settings, goes nowhere else, and is sent nowhere at all. Key units rather than pixels, so changing the keyboard height keeps it.
Touch, in settings, turns on an overlay that draws all of it on the keys themselves. Each key gets a line and a dot to where your taps for it land, a shaded box for the part no spelling model may overrule, an oval for how loosely you hit it against your own average, and the number of taps that rests on. Four toggles turn those layers on and off, and they take effect while you look at them. Faint means barely measured. The same page holds Reset touch model.
It draws on the real keys because the keyboard is the only thing on the phone with the right geometry to explain itself on. A diagram on a settings page has to invent a keyboard to draw, and then share a screen with the real one.
Whether it helps you is not something a benchmark can promise. A simulated typist who lands two thirds of a row low reads at 88.6% with it and 69.4% without, and an accurate typist is left alone. But simulated typists are not people. Unlike the swipe work there is no corpus of real taps to check this against. If it ever feels wrong, reset it.
Two decoders ship, and the setting that picks between them is under Swipe settings.
The model is FUTO Swipe's encoder (arXiv:2606.25247): a 635k-parameter temporal convolutional network, 2.6 MB, a couple of milliseconds a swipe. It reads a traced path and says, at each of 32 points along it, which letter it thinks was being aimed at. It is layout-agnostic — the key positions are an input rather than something it learned — so one file serves QWERTY, AZERTY, QWERTZ and all three height presets, with no per-layout table anywhere.
Turning that into words is a trie-constrained CTC beam search
(NeuralDecoder), which is this
repository's own code, written from Equations 2 and 3 of the paper. Nothing about it is guesswork:
app/src/test/resources/swipe_goldens.json holds ten emission matrices recorded from the real model
over real human swipes, and the tests pin the search's output to a reference implementation
word for word.
The shape matcher (GestureDecoder)
is the SHARK² family: draw the ideal path through each candidate word's keys, and ask which one the
trace most resembles, across a location channel, a shape channel and a length channel. It is still
here and still loaded, because it is what answers on the first swipe after an install — the model is
copied out of the APK before it can be loaded — and on any phone where the native library will not
load at all. It also has settings the model does not: how far it reaches when nothing matches.
517 real human swipes from FUTO's corpus (MIT), decoded against this app's own 63k dictionary, both decoders given the same traces in the same layout:
| Right first time | In the top four | |
|---|---|---|
| Shape matching | 74.7% | 90.7% |
| Model | 92.5% | 99.2% |
The second column is why the delete key walks the alternatives: the word you drew is nearly always reachable, so how many guesses it will offer (2 to 8, under Swipe settings) matters more than the first column does.
assets/swipe/encoder.pte is FUTO's published encoder with one change. Its third input is a boolean
mask saying which of its 64 key slots hold real keys; that has been baked in as a constant of 26 true
and 38 false. The keyboard's alphabet is never going to be anything else, and ExecuTorch's Java API
cannot construct a boolean tensor at all, so a program that asks for one cannot be driven from
Kotlin. The rewrite touches the program's value table only. No weight is changed, and the modified
program's output was checked to be bit-identical to the original's.
The model weights are FUTO's, under the FUTO Model Weights License 1.0, which permits this use and requires the credit that appears under Swipe settings. The corpus the numbers above are measured on is MIT. Everything else here stays MIT, and none of it touches the network: the model runs on the phone, like the dictionary does.
About 6 MB of APK: 2.6 MB for the model and the rest for ExecuTorch's arm64 native library. Nothing
is downloaded — the Light Phone III is arm64, and abiFilters keeps every other architecture out.
./gradlew :app:assembleDebug # debug build
./gradlew :app:assembleRelease # release build (unsigned unless signing env vars are set)Needs JDK 17 and the Android SDK (API 35). Every push is built and tested by
.github/workflows/build.yml.
The two bundled data files are generated, not hand-written — see tools/:
res/raw/charmodel.bin— character trigram model for per-tap key selection (tools/gen_charmodel.py)res/raw/words.bin— the word list behind autocorrect and swipe typing (tools/gen_dict.py)res/raw/bigrams.bin— the word-pair table that ranks candidates against the preceding word (tools/gen_bigrams.py)
The typing logic lives in app/src/main/java/app/lightphonekeyboard/text/ — dictionary, autocorrect,
swipe decoding, prefix completion, personal word list — and deliberately has no Android
dependencies, so it is unit-tested against the real dictionary rather than only on a phone.
The signing key is a repo secret (KEYSTORE_BASE64), never a file in the repository. Only its
certificate fingerprint is committed, in signing-fingerprint.txt, and CI fails if a build's
certificate doesn't match it — Android identifies an app by (packageName, certificate), so a changed
certificate silently breaks every existing install's ability to upgrade, surfacing only as
Failure: Invalid at install time. Release builds are never left unsigned: the Gradle build fails
outright with no key rather than producing an APK that looks fine and then refuses to install.
To build a signed APK yourself, either set KEYSTORE_FILE, KEYSTORE_PASSWORD, KEY_ALIAS and
KEY_PASSWORD, or drop a keystore.properties in the project root:
storeFile=/path/to/your.jks
storePassword=…
keyAlias=…
keyPassword=…Debug builds share that key and the same applicationId (no .debug suffix), so adb install -r
replaces an installed release in place rather than leaving a second Bright Keyboard in the input-method
list. Coming from the upstream app, or from a build made before this key existed: Android refuses the
update because the certificate differs — uninstall the old one first.
- The weights in
Corrector.ktandGestureDecoder.ktwere fitted against the benchmarks inapp/src/test. If you change them, run./gradlew :app:testand look at what you traded away — these tests exist precisely so tuning isn't done by ear. - Every key-unit coordinate in the typing/gesture code is measured from the top of the first key row, not the view, so the suggestion strip's height doesn't shift gesture math.
bigrams.binholds no word list at all — its key is a 32-bit FNV-1a hash ofleft \0 right, computed independently intools/gen_bigrams.pyand inContextModel.fingerprint. If those two ever drift apart, every lookup returns 0, context ranking silently switches itself off and nothing else fails.ContextModelTestasserts known-good pairs out of the shipped file precisely to catch that.- Verifying a release APK without an Android SDK: AGP renames resource paths in release builds
(
res/raw/words.binships as something likeres/x5.bin), so grepping the zip for a filename proves nothing — match on size + SHA-256 instead. The APK is v2/v3-signed only (noMETA-INF/*.RSA); check for theAPK Sig Block 42magic located from the EOCD central-directory offset instead. - PRs against typing/dictionary logic should include or update a JUnit test in
app/src/test— that suite is what lets this be tuned without an Android device.
Every push to main builds, tests, and publishes a signed APK as the next v1.2.<n> release (n is
the CI run number) — see .github/workflows/build.yml. Obtainium picks
it up on its own. A push can bundle more than one commit; only the push's final commit carries the tag.
-
v4.6.x (2026-09-23) — The keys learn your hand, and then stay put.
The invisible key targets moved too easily. Each key learned on its own from about its last 17 taps, so 26 targets wandered separately and never settled.
One shared miss, small corrections per key. Most of a miss is the same on every key: the thumb lands low and a little to one side. That part is now learned once, from every letter, and each key adds a small correction of its own, at most 0.08 of a key. A key you have never tapped still gets the shared part.
Learning slows down. The first taps teach quickly. After that the rate drops to a floor about twelve times lower than before. On the worst case in the tests (40 taps in a row, all on one key, all a fifth of a key off), a settled key moves 0.04 of a key. Before, it moved 0.18.
Tighter limits. Learning can move a target at most 0.2 of a key from where it started. A tap more than 0.6 of a key away moves no target. It still counts toward how widely that key is hit. One key's target can be at most 10% wider or 8% narrower than your average, down from 25% and 15%.
Settings → Touch → How much it learns: Off, Gentle, Normal. Gentle is the default. Normal works the way 4.5 did. Off uses none of what was learned but keeps it, so switching back restores it.
Your existing learning carries over. The average of what your keys had learned becomes the shared miss, and each key keeps its own centre. The saved format is now
v5. On the simulated typist who lands 0.65 of a row low, accuracy is 89.5%, up from 88.6%. -
v4.5.x (2026-09-23) — A moved keyboard stays close, and its keys got bigger.
A narrowed keyboard could be dragged hard against one edge with a third of the screen empty beside it. It now shifts only a little either way: neither side may be left more than 22% empty. The narrowest it goes is 70% of the screen, up from 55%, where ten letters were about 5 mm wide.
Bigger targets when narrowed. The keys get taller to make up half of the width they gave away, so a 70% keyboard has keys 15% taller. The edge keys also take the near part of the empty strip, so a thumb that overshoots q or p types q or p.
Fixed:
- Lifting one finger at the end of a pinch made the keyboard jump sideways. The drag now continues from the finger that stays.
- With the keyboard centered, the reset button's target covered the rightmost keys (p, l, delete, return). It now sits only in the empty strip, on whichever side has room.
- On a keyboard narrowed by pinching, the reset button did nothing. It now puts the width back.
- A sideways drag did not move the keys until the keyboard also moved up or down.
- Dragging holds at the center for a few dp, so it is easy to put back.
-
v4.4.x (2026-09-22) — An address is not a misspelling.
Typing a website into WebTools' address bar had autocorrect fighting every part of it, and the suggestion strip offering English words for a hostname. The keyboard read the field's type to pick the numbers layer for a phone number and then ignored it for everything else.
FieldKind.correctablereadsinputTypethe way Android's own keyboard does. Autocorrect and the strip stay off in a URI or email field, any kind of password, a list filter, and any field whose app setTYPE_TEXT_FLAG_NO_SUGGESTIONS; a field that declared nothing is prose, as it always was. WebTools declares its address bar as a URI, so this is the half that was missing. Auto-capitalization already followed the field (a URL field asks for none). Five tests.Also: the show/hide broadcast now carries the keyboard's height, so BrightControl 4.37 can end its edge-swipe strips where the keys begin.
-
v4.3.x (2026-09-22) — Hide means hide.
Pressing the hide-keyboard key typed a space. It was worst with the mic turned off, because that puts Hide directly beside the space bar, and a video of it arrived on Discord.
The cause was the touch model doing exactly what it was built to do, one key too far. Space, return and delete learn where a typist actually lands on them and move their hit rectangle to meet the finger. The sideways correction is kept in units of the key's own width, so a stored model means the same thing at every keyboard size — and on a bar five cells wide the allowed correction came to a cell and a half, which is the whole of the one-cell key next to it. A hide key that is not tracked had no way to hold its ground.
Two changes. A tap on the painted face of any control key types that key, the same promise a letter's core has always had; only the gutter around it is open to a learned target. And a big key's learned reach is capped at half a letter width in pixels (
TouchModel.bigKeyShift), which is as far as a systematic miss goes before the finger is on the next key. Learning is unchanged; what changed is how far it is allowed to act. -
v4.2.x (2026-09-21) — Text faces, and somewhere to keep your own.
A sixth tile on the tools page: Faces. 126 kaomoji in seven categories — Happy, Love, Sad, Angry, Unsure, Animals, Doing — six to a page, with the ones you used lately at the front. Tap one and it goes in, and the page stays open, because faces come in twos and threes.
Search finds them by name. A kaomoji has no name of its own the way an emoji does, so every bundled face carries one:
shruggives¯\_(ツ)_/¯,tablegives(╯°□°)╯︵ ┻━┻. The search key borrows the letters and return runs it, the same as emoji and GIFs.Add your own under Settings → Text faces. They sit in a category at the end of the page and come first when you search.
Each face is checked against the phone's font one code point at a time.
Paint.hasGlyphasks whether something draws as a single glyph, which is the right question for an emoji and the wrong one for five characters of punctuation — asked of a whole face it says no every time. Combining marks are skipped, because a lone accent has no glyph of its own in most fonts and still draws perfectly well over the letter in front of it. -
v4.1.x (2026-09-20) — Installing a language now does something.
It did not. Two people reported it on the same day, one in Hindi and one in French: the language downloads, the tick appears, and autocorrect carries on correcting to English.
The dictionary was loaded once when the keyboard's process started and never again. Installing a language wrote the files and changed the setting, and nothing told the running keyboard to look. It went on holding English until Android happened to kill the process. The character model had the same fault for the same reason. Both are now keyed on the set of chosen languages, rebuilt when that set changes, and the keyboard checks when it opens on a new field.
Anything cached for the life of a process needs a stated reason to be dropped. Neither of these had one.
Also fixed: Indonesian and Icelandic could not be found. The language search lists what the AOSP repository publishes, and those two exist precisely because it publishes no word list for them, so the one screen that should have offered them was the one screen that could not.
-
v3.10.x (2026-09-19) — Move the keyboard, resize it, and two languages nobody else has.
Tools → Size and place. Drag the keyboard where you want it. Pinch to resize it. It can sit narrower, taller, shorter, off to one side, or lifted clear of the bottom edge, and it stays where you put it. One-handed mode was the same idea with two presets, and still works. This is the version without presets.
Everything is stored as a fraction of the screen, not in pixels. It survives a rotation and means the same thing on a different phone. A pinch reads its two axes separately, because people pinch diagonally and mean one of the two.
Indonesian and Icelandic. The AOSP repository publishes no word list for either. These packs are the only way to get them.
Both are smaller than the rest: 16,639 words and 10,706, against 64,000 to 70,000. That is what the available spelling dictionaries accept. A shorter checked list beats a longer one carrying misspellings, because a typo in the dictionary can never be fixed.
-
v3.9.x (2026-09-19) — Search for a language, and run several at once.
Settings → Language now searches the dictionaries published at aosp-dictionaries, where keyboards in this family get theirs. Eighty-odd languages. Type a name and tap it. The phone fetches that word list from that project and builds a dictionary out of it.
Nothing ships installed but English, and nothing is rehosted here. Those lists come from many sources under several licenses, and passing them on inside somebody else's release is not this project's call to make.
More than one can be on at once, each a tick you turn on and off. This is an honest trade, not a free win. A word list says how common a word is within its own language, so two at once says either could be the one you are typing. Anyone switching on a second language is making that claim about themselves, which is why it is a tick and not a single choice. Switch them all off and English comes back, because something has to be there.
The phone builds the character model too, from whichever languages are on. There is no file to download for "Spanish and Norwegian together", and that table breaks a tie between two keys under one thumb. It ports the generator that made the English one, and every one of its 19,683 cells matches that generator exactly.
A language whose letters are not a to z says so when you try it, rather than installing and then turning out to hold nothing.
-
v3.8.x (2026-09-19) — Six more languages, fetched when you ask for one.
Settings → Language offers Spanish, French, German, Portuguese, Italian and Norwegian beside English. Tap one and the keyboard downloads its dictionary, about half a megabyte. Long press an installed one to get the space back. English is built in and cannot be removed.
One language at a time, deliberately. Two in a single ranking means a word that is common in the other one outranking the word you meant in this one.
The keys do not change. A pack carries a word list and a character model, not a layout, because a missing accent key costs nothing: type
cafeand getcafé, typeoland getøl. Everything under a tap is a to z, so each pack stores its words folded and keeps the written spelling beside them. Pick the key arrangement itself under Keyboard layout, as before.A pack brings its own character model too. The letter that follows
thin English is not the one that follows it in Norwegian, and that table breaks a tie between two keys under one thumb.tools/gen_pack.pybuilds them from two sources, the same split the English list uses. wordfreq says how common a word is. The language's hunspell dictionary says whether it is a word at all. A corpus on its own carries its own misspellings, and a typo in the dictionary can never be fixed.It also reads AOSP
.combinedword lists, the format aosp-dictionaries publishes for a hundred-odd languages. Check the license before shipping a pack built from one. That repository names a different source per language and they are not all alike.The download is the second thing in this keyboard that touches the network, after the GIF button. Typing still never does.
-
v3.7.x (2026-09-19) — Import a word list, and accented words work at all.
Words → Import a word list takes a plain text file, one word to a line. It ignores anything after the word on a line, so a frequency list or a spreadsheet column works as it is. This is for the case the hand-typed list is not for: a glossary, a field's jargon, a cast of characters, a vocabulary. A second import replaces the first, because the second file is nearly always the same file with a fix in it.
The imported words live in a file, not a setting. Settings stay in memory for the life of the process, and a glossary runs to tens of thousands of words. The screen counts them rather than listing them. Everything below that point treats them exactly like a name you added by hand.
Fixes accented words in the personal list, which never worked. The keyboard keyed a word by its own spelling, and the searchable dictionary holds a to z. So it accepted "Zoë", listed it, and then dropped it on the way into the dictionary. It sat there looking added and did nothing. Keys now use a folded spelling: type
zoe, getZoë.That folding is the groundwork for language packs. Everything under a tap is a to z. The trie branches twenty-six ways, the character model is a 27 by 27 by 27 table, and the swipe model emits twenty-seven classes. None of that widens cheaply, and none of it needs to. Across the six languages being prepared, folding the accents away leaves 0.6% to 4.5% of words sharing a key, which the alternatives list already sorts out by frequency.
-
v3.6.x (2026-09-18) — Delete can take a whole swiped word out.
Under Swipe settings, Delete after a swipe is now a choice. Walk the readings, the default and what it has always done, or take the whole traced word back out in one press.
One gesture puts a word in, so one press taking it out is the symmetric undo. When a trace reads nothing like the word you drew, stepping through three more wrong ones to reach a delete is the long way round. The leading space goes too, since the next swipe puts its own back.
Nothing is lost by choosing it. The other readings are in the suggestion strip, where a tap picks one directly.
It does not change what delete does after autocorrect fixes a typed word. That is still Cycle or Revert on the autocorrect page. The choice there is between offering the readings and putting your own spelling back, and a traced word has no spelling of yours to put back.
-
v3.5.x (2026-09-18) — Shift and 123 learn, and the delete signal works again.
Shift and 123 join space, return and delete in the touch model. Shift is letter-sized and sits right beside Z. That boundary is worth getting right.
They needed a rule of their own. The model learns from any tap you do not delete, and these two put nothing on screen to delete. An accidental shift would have counted as a good tap and pulled the key further into its neighbor. Pressing shift straight back off, or going 123 then ABC, now counts as taking it back.
Fixes the delete signal, which v3.4 broke. Parking a tap folds in the one before it. v3.4 parked the backspace before settling the verdict, so the letter being deleted was fed to the model as a good tap, and the veto then threw away the backspace instead. Every tap was learned whatever you did about it. That is the one thing this design exists to avoid. A test now walks the sequence.
Also: the autocorrect settings rows share the space at the bottom of the screen rather than leaving it black. Each one is a larger target for it.
-
v3.4.x (2026-09-18) — Space learns too, and the letters can come off.
Space, return and delete are now in the touch model. They are missed in ways a letter is not. They sit at the edges. They are reached rather than aimed at, and the miss runs mostly one way.
Each one learns where your taps for it land, and the boundary with its neighbors moves by that much. A letter's anchored core still wins outright, so a growing space bar cannot cost you a letter you hit squarely. Their sideways offset is measured against their own width, not a letter's: a space bar is five cells across, so a miss in letter-widths would mean nothing.
Hide the letters. A switch on the Touch page takes the labels off. It leaves a bump under F and J, the way a real keyboard does, and space, return and delete stay where they are. This is the point of all the above, said out loud. After a while you are not reading the keyboard. You are reaching for it. Nothing has moved, and the press flash still lands where you touched.
The toolbox page has a back key. Its bottom row used to be the keyboard's own: space, return, ABC. That made a page of tiles look like somewhere you could type. One key across the bottom now.
The key beside space can be the emoji key. Toolbox, emoji, or nothing, under Emoji settings. Anyone who only ever opened the toolbox to reach emoji was paying two taps for one.
-
v3.3.x (2026-09-18) — The overlay moved onto the keys.
v3.2 drew the model as a diagram on a settings page. It did not fit, and it could not. A picture of a keyboard has to invent a keyboard to draw on. Then it has to share a screen with the real one.
The overlay now paints on the real keys, at real size, wherever you are typing. Touch in settings holds the switch, the four layer toggles and the reset. The keys are the picture.
-
v3.2.x (2026-09-18) — Look at what it learned.
A Touch page in settings that draws the model: every key's target, its anchored core, its spread, and the taps each one rests on, as four layers you can switch off one at a time. There is a field on the page, and typing in it moves the picture, because the page borrows the keyboard's live model rather than reading a saved copy. Reset touch model moved here from the autocorrect page, where it sat next to nothing that explained it.
This exists because of what v3.1 is. A keyboard that adapts silently, keeps what it learns for good, and never moves a key on screen is the right design and an unaccountable one: there is nothing to look at, and no way to tell a keyboard that has learned your hand from one that has quietly gone wrong.
Also fixes v3.1, which learned nothing. The check that spotted a reset from settings read an absent setting as "somebody cleared it", when on a fresh install it only ever meant "nothing written yet" — and the same branch disarmed saving, so the setting stayed absent and the check fired again on the next field, for ever. Every new install lost its model at the end of the first field it typed in. The rule is now four lines in
ModelStorewith the sequence written out as tests, which is where it should have been: it cannot be exercised on a phone without deliberately breaking one.Two smaller ones found with it. The map drew the bottom row a third too wide, because shift and backspace share that row with the letters and hold a cell each. And the summary counted the population starting point as if it were something learned, so a keyboard that had measured nothing still reported having moved.
-
v3.1.x (2026-09-18) — The keys learn where your fingers go.
Every letter now has its own target: where your taps for that key land, and how widely they scatter. The keyboard learns both from your typing. Neither is visible. The keys sit where they always did and never move or resize. See Where your keys are.
What it replaces was one vertical offset per row. Three numbers for the whole keyboard, which could not say that a thumb undershoots
qand overshootsp. It could not say that you hit some keys more precisely than others either, so every key claimed the same ground regardless.The part worth knowing is what counts as evidence. The obvious rule is to learn from the taps the keyboard resolved without help. That rule is wrong twice over. It feeds on itself, because a tap moved to a neighbor is then credited to the neighbor. And it only ever sees taps that landed nearer this key than any other, so every key measures the same width by construction. The rule now is simpler: a tap teaches the model unless you delete it.
Two guarantees came with it. The middle 72% of every drawn key is anchored, so nothing overrules a deliberate tap. And the model is stored in key units, so changing the keyboard height no longer throws away what it learned. Reset touch model, under Autocorrect settings, clears it.
Also fixed: every cold start saved the model before reading it, so nothing survived one process lifetime. That bug was in the old per-row version too.
-
v3.0.x (2026-09-18) — Three-point-oh.
A version number, not a rewrite. The 1.x line was a faithful clone of the Light Phone's own keyboard with autocorrect bolted underneath; what it is now — a neural swipe decoder, a tools page, a clipboard, one-handed mode, a full-screen GIF picker — is a different thing wearing the same face, and the number had stopped saying so.
The package name is unchanged, so this installs over any earlier copy as an ordinary update.
-
v1.9.x (2026-09-18) — The keyboard presses paste for you.
Where a field declares no image support the GIF still goes on the clipboard, and the keyboard now presses paste itself rather than telling you to. "It's on the clipboard, go and paste it" is a chore, not a feature.
The clip had to change for that to be safe. One holding only a
content://URI is coerced to text by any field that cannot take an image, so an automatic paste into a plain text box would have typedcontent://app.lightphonekeyboard.gifs/gifs/gif-…into somebody's message — worse than the link this whole path replaced, since it means nothing to a reader and the permission behind it expires in minutes. The clip now carries an explicit empty string beside the URI: an app that handles images gets the picture, an app that only pastes text gets nothing, which is the right amount of nothing.The paste is a try and stays one. There is no way to ask an app whether it will accept one and no signal afterwards saying whether it did, so the clip is left on the clipboard either way.
-
v1.8.x (2026-09-18) — The GIF page fills the screen, the GIFs move, and you can keep one.
Choosing between pictures through a slot an inch tall is choosing blind. The page now takes most of the display and hands it straight back on the way out, with the number of rows coming from the height rather than a constant. Cells are sized to fill that height — forcing them square spent the whole page on cell size and left a band of black under the last row, fitting six GIFs on a page that used to hold nine.
Every GIF is drawn whole, fitted inside its cell rather than cropped to fill it. Cropping cut the ends off every wide one, and a GIF is usually wide because the thing that makes it funny is at one end.
They animate, which needs API 28 against this app's minimum of 26 — below that, and whenever a decode fails or the file is large, the still frame is drawn and everything still works. Only the page on screen ever runs, and every way off it stops them.
Hold a GIF to star it. Starred ones come first when the page opens, and the star key shows only them — the one part of the picker that works with the radio off. Cells had to move from committing on touch-down to committing on the lift for this: a hold cannot be told from a tap until the finger goes.
-
v1.7.x (2026-09-18) — A GIF goes in as a picture, and a search waits for return.
A keyboard cannot put an image into an app that has not said it takes one:
commitContentis the only route Android offers andEditorInfo.contentMimeTypesis the app's answer. What could be fixed is what happened when the answer was no, and committing the URL was wrong — a link is not what anybody picking a GIF asked for, and in the fields that decline (a note, a search box) it is no use to anyone either. It now goes on the clipboard as a real image clip, and the keyboard says so. The apps where sending a GIF makes sense are the ones that declare the type, so the picture path is the common one.Saying so needed somewhere to say it. A keyboard has no dialog, a toast from one is unreliable, and the field belongs to you — so a message borrows the suggestion strip for two seconds, the way a search already borrows it, and gives it back.
Search now runs on return. Searching on every letter was wrong in both panels for different reasons: the emoji grid reshuffled under your thumb while the word was still half typed, so the thing you were reaching for moved, and every GIF keystroke was a network request with three of them abandoned before the one that mattered. Letters, digits and spaces build the query — a space used to end the search, which it cannot now, because "happy birthday" is the search people type.
-
v1.6.x (2026-09-18) — The swipe model is on by default again, because a crash is now detected rather than feared.
It was switched off in 1.5 for a good reason: the model is a native library, a fault inside one raises a signal rather than an exception, and the process is simply gone. Nothing in Kotlin catches that. For an ordinary app it is a bad crash; for a keyboard it takes the keyboard out of every text field on the phone, including the ones you would need to turn it off again.
So it is not caught, it is recorded. A flag goes to disk before the risky part and is cleared after it, with
commitrather thanapply— the process is about to be killed by a signal, and a write still sitting on a background thread is a crash nobody heard. Finding the flag still set at the next launch means the attempt never came back, and nothing else can leave it that way, because the clearing is unconditional. Two of those and the model is left alone until you switch it back on, which the Swipe settings screen says in as many words.The armed window covers a warm-up run, not just the load. Inference is at least as likely to fault as loading is, and one pass inside the window catches a model that crashes on use here — on a background thread, before anybody has swiped — rather than under your finger, where it would take the word you were writing with it.
The counting rule is four lines and every one of them is an off-by-one waiting to happen, so it lives in
text/CrashBreaker.ktwith a test that walks the sequence a phone actually goes through. Giving up one attempt early takes a working feature from everybody; giving up one late is two more crashes. -
v1.5.x (2026-09-18) — A GIF picker, and a tools page with only tools on it.
The tools page had Height, Settings and Symbols on it. All three open somewhere else, two of them an Activity — which means leaving the field you are typing in — and all three already sit in the app's own settings, where somebody looking for them looks. A page of shortcuts to elsewhere is not a toolbox. It now holds five things that can only be done from here: Clipboard, Emoji, GIFs, One-handed and Hide keyboard.
GIFs are BrightChat's picker, ported: the same KLIPY client and the same parser, which were already free of Android and already tested, so the tests came across unchanged. New here is a paged three-by-three grid of first frames, a search that borrows the letter keys exactly as the emoji search does, and the insertion.
Insertion has two honest outcomes. A field that declares
image/gifgets the file, through a FileProvider with a read grant. A field that does not — most of them, because most fields are for text — gets the link. Doing nothing was the only outcome that would have been wrong, and when even the link cannot land the picker comes back and says so.This is the first thing in the keyboard that uses the network, and the claim at the top of this file has changed to match. Typing still never does.
-
v1.4.x (2026-09-17) — Swipes are read by a neural model now. 74.7% right first time became 92.5%.
Swipe decoding has been a SHARK²-style shape matcher since v1.0: draw the ideal path through each word's keys, and score the trace against every one of them. That is what most gesture keyboards still do, and it is good at clean traces and bad at fast ones. It now sits behind FUTO Swipe's encoder, a 635k-parameter network that reads the path directly. Measured on 517 real human swipes with this app's own dictionary behind both decoders: 74.7% right first time before, 92.5% after, and 90.7% to 99.2% in the top four the delete key walks.
The model is layout-agnostic, which is the reason it fits here at all: it takes the key positions as an input, so the same 2.6 MB serves QWERTY, AZERTY, QWERTZ and every height preset, and the keyboard needs no table per layout. Reading words out of what it emits is a trie-constrained beam search written from the paper, and the tests pin it to a reference implementation on ten emission matrices recorded from the real model.
The old decoder has not been removed and is not dead code. It answers on the first swipe after an install, while the model is still being copied out of the APK, and on any phone where the native library will not load — and Shape matching under Swipe settings switches back to it for good.
Traces now carry the time of every touch point. The model reads speed and acceleration out of the spacing between points, and this keyboard's own spacing is not the hardware's: a batched move hands over several positions at once, and points too close together are dropped. Without the times, a pause and a sprint through the same letters arrive as the same stroke.
The APK grows about 6 MB, 2.6 MB of it the model. Nothing is downloaded and nothing leaves the phone.
-
v1.3.x (2026-09-17) — Renamed to Bright Keyboard. A tools page behind the emoji key, with a clipboard history and one-handed mode. A key that hides the keyboard.
The repository is now
gi-os/BrightKeyboardand the app is called Bright Keyboard, which puts it with the rest of the Bright* set. The package name has not changed, so this installs over an existing copy as an ordinary update. Old GitHub links redirect.The emoji key became a tools key. One keyboard key cannot lead to five things, so it now opens a page and the five things are on it. The page's bottom row is a copy of the row you pressed the key in — same keys, same widths, same order — with the emoji key where the tools key was. Emoji cost one more tap than before, but the second tap is in the same place as the first, which is one movement rather than two. Holding the tools key still swaps keyboards, as holding the emoji key did.
Clipboard history. The last 24 clips, three to a page, pinned ones kept. Paged rather than scrolled: the emoji grid scrolls, and that scroll already shares a finger with swipe typing and with swipe-to-dismiss. Two arrows cost one row and no ambiguity, which on a list this short is the better trade. Clips marked sensitive by the app that copied them are not recorded, and nothing here is sent anywhere.
One-handed. The keys take four fifths of the width and sit against whichever edge you choose. The empty strip is a single button that puts them back. Swipe typing needs no adjustment for it: the decoder is fed key positions measured from the laid-out keys, so a narrower keyboard is just a different set of positions.
A hide key, off by default, in the bottom row beside Return.
-
v1.2.x (2026-09-13) — Every emoji, with search. A swipe decoder that reads corners. A key-vibration switch. A hold on the emoji key to swap keyboards.
The emoji panel held 24 glyphs, written out by hand in the view. It now holds all of them. 1,761 base emoji come from Unicode's own data, in Unicode's order and categories, with the CLDR keywords that make search work.
tools/gen_emoji.pybuilds the table, and it comes to 117 KB.The font decides what appears. A bundled table lists what Unicode defines, not what the phone can draw, and those are never the same list. LightOS ships whatever emoji font its Android version shipped with, so anything newer renders as an empty box. The panel asks the font about every sequence once, on a background thread, and drops what it cannot draw. That also keeps the panel right if LightOS updates the font. It recomputes the answer rather than remembering it.
Search. Press the magnifier and the letters come back. What you type goes into the query, not into your message. Results fill the grid as you go.
pizzafinds 🍕. So doeshungry, because CLDR knows that is what people call it. A setting does the other way round. With Emoji in suggestions on, typing the name of a thing puts its emoji in the suggestion strip. The common ones then never need the panel. It fires on a whole word, never on a prefix. That is what keeps it quiet.Ranking matters more than matching here.
handmatches sixty emoji. An exact name wins, then a word of the name, then a keyword. Unicode's order breaks ties, and that puts the familiar emoji first.You pick a skin tone once, in settings, and the panel uses it throughout. Hold any emoji to see all of its variants: every tone, and the gendered forms. The table stores those spellings rather than assembling them at run time. Unicode spells tone as a modifier, and gender sometimes as a joiner and sometimes as a different character. Assemble one wrongly and no font has it, so it draws as a box.
The panel scrolls, with a jump button per category and the emoji you used lately at the top. It builds only the rows on screen, so scrolling 220 rows costs what scrolling three costs.
The swipe decoder reads corners now. A corner is the one place on a trace where what you meant is unambiguous. Everywhere else the finger is in transit between two letters, and its position is a compromise. The old model averaged every sample equally, so the long straight runs in between diluted that evidence. A wrong word with roughly the right sweep could beat the right one. Each corner now charges a candidate the distance to its nearest letter. A word with a letter at every turn pays nothing.
The preceding word also gets more say on a swipe than it does on a tapped correction. The two are not the same bet. A correction replaces a word somebody typed deliberately, so it answers to a short leash. A trace is ambiguous by nature, and context is the only thing that can separate its readings.
Measured over 250 real word pairs. The corner channel and the looser bound, together:
before after clean 99.6% 100.0% jittery 97.6% 98.4% sloppy 94.8% 95.6% fast 96.8% 97.2% fast + sloppy 92.4% 93.6%The benchmark in the tests fitted both numbers, which is the only honest way to set any of these. One thing turned up on the way. The corner channel has to stay off for two-letter words. A two-key stroke is one straight leg with no interior corner, so anything found on it is noise. Charging that noise made
itdecode asour.Swipe typing gets its own settings, the two questions the autocorrect page asks about taps. How far it reaches decides what the decoder does when nothing matches: settle for the nearest word in the dictionary, or give you nothing. Words delete offers has no equivalent on the tapped side. It is here because of the numbers: the word you drew is in the first four about 99% of the time, and first about 94%. If your traces run sloppy, accuracy is not the lever that helps. What helps is how many guesses the delete key can walk before you give up and retype.
Key vibration now has a switch. The tick under each key press was unconditional, and stopping it meant turning off haptics for the whole phone. Android's own touch-vibration setting still outranks it.
Holding the emoji key changes keyboard. The globe key does the same job. It appears only when you have a second keyboard, and it has a switch of its own. This route is always there.
-
v1.1.x (2026-09-13) — Four engines instead of one, a delete key that walks every guess, and a twelve-key T9 keypad.
Autocorrect used to ask one question: how near is this word to a real one on the keys. That model catches a thumb landing one key over. It is blind to everything else. Three more engines now run beside it, and their answers merge into one ranked list.
A sound-alike index reads the word the way it is said.
nitefindsnight,fonefindsphone,enuffindsenough. None of those sit near their target on the keyboard, so the original engine could never reach them. The index is not Metaphone. Metaphone and Soundex match names, and both throw the vowels away. Without vowels,nitelands in a bucket withnot,netandnut, and the commonest of those wins. This keeps a normalized vowel for every run, which cuts the average bucket from forty words to about five.A missed-space search reads a word as two.
alotbecomesa lot,inthebecomesin the,thankyoubecomesthank you. A word-at-a-time corrector cannot do this at all. It only ever sees one word, and one word has no space in it to find. English is full of compounds whose halves are both real words. So the search refuses any word the dictionary knows. Without that rule,nothingbecomesno thing.A shortcut table holds the corrections that are facts rather than search results. Missing apostrophes are most of it here. The apostrophe sits on the symbols layer. Reaching for it costs two taps and a layer switch, so people skip it.
dontis nowdon't. The table has a second tier that only ever offers.thxandnitestay exactly as typed, withthanksandnightone delete press away, because someone who writesthxmeansthx.Two engines reaching the same word independently is strong evidence, and the ranking says so. One measures key distance and the other measures pronunciation, so agreement between them is two kinds of evidence, not two views of one.
Autocorrect settings is a new page. How much it fixes has three settings. Cautious commits only clear typos and apostrophes. Balanced adds sound-alikes and missed spaces. Eager commits on a slim margin. The setting changes only what the keyboard commits on its own. Every candidate every engine found stays in the delete key's list at all three. Turning it down makes the keyboard quieter rather than less capable.
Whether a correction commits at all is now a margin over the runner-up, not a score on its own. A correction is safe when it is the only good reading of what you typed. Two plausible readings within a hair of each other mean the keyboard does not know, and the honest move is to leave the word alone.
The delete key now walks the alternatives for tapped words too. It has always done this after a swipe. After a tapped correction it only had a single undo. There was no better reason for that than the age of the two code paths. One press gives the next-best word. Another gives the one after. The last stop is always exactly what you typed. The second new setting, the delete key, turns that off for anyone who finds it surprising. Undo the fix puts your own spelling back in one press and stops there.
A keypad (T9) layout, next to QWERTY, AZERTY and QWERTZ. Three letters to a key, one tap per letter, and the dictionary works out the word from the digits. The word appears in the sentence as you tap, and every key replaces it in place. You read it where you are writing, not in a list above it. Finishing the word opens the same delete-key list. Every keypad word is ambiguous by construction. Saying "no, the next one" without retyping is what made T9 work at all.
The keypad corrects as well as matches, which traditional T9 never did. Miss a key there and the word is unreachable. You have to notice, delete and retype. Here one wrong digit still finds the word, so
84663givesphone. A word you type exactly never loses to a corrected one.You can also swipe on the keypad. A finger cannot express a doubled key, because it is already on it, and this is where every published attempt at gliding on a keypad has stopped.
hellois4-3-5-5-6tapped and4-3-5-6traced. The dictionary is indexed under both, so the doubled key stops being a special case.Multi-tap is there for people who want no prediction at all. Press 2 three times for
c. It types the letter you pressed and nothing else.A globe key appears on the bottom row when the phone has more than one keyboard enabled. It is absent when there is nowhere to go. A bottom row is too narrow to spend on a key that does nothing. Tapping it moves to the next keyboard.
One bug found while testing, worth writing down because nothing crashed. The keypad packs its digit index into a signed 64-bit integer. At the depth it used, the top digit landed on the sign bit. The range search for a leading
8then came back empty. 8 carriest,uandv. So the first tap ofthe,to,that,thisandtimeshowed nothing, and the word appeared only on the second tap. Every test passed, because every test used two digits or more. -
v1.0.x (2026-08-01) — A login code arrives in the strip, and closing needs a longer hold. A verification code arrives in one app and is wanted in another, and the walk between them is done from memory in the one moment you cannot afford to mistype. iOS puts it above the keys; Android does it through autofill and
SmsRetriever, both of which need Play Services and a real SMS app — and on this phone the codes arrive over BlueBubbles rather than the carrier, so neither would ever see them. The keyboard is the only surface present in both apps, so it reads the code from BrightChat's provider and pins it, in the empty-prefix case only. Committed verbatim through a newStripItem.verbatimflag rather than a special case in the IME, because every ordinary suggestion path does two things that are wrong for a code: it takes the case of what was typed before it (rewritingG4T7QXasg4t7qx) and it appends a trailing space (a seventh character in a six-character field). Holding it does nothing, since nothing was learned. Separately: the hold before a downward swipe closes the keyboard goes from 180ms to 500ms. 180 was tuned against swipe typing and not against the hand — it is shorter than an unhurried tap, so a slow press near the bottom row that drifted down as the thumb rolled off could clear the hold and the 30dp together and close the keyboard mid-word. -
v1.0.21 (2026-08-01) — Swiping i-m gives "I'm", not "jim"; swipe-down-to-close needs a hold first. "im" was reachable in the dictionary but scored on its own entry, which tools/gen_dict.py deliberately floors at -13.0 for chat shorthand nobody actually types — read as "I'm" it isn't nearly that rare, so the floor lost every time to "jim"/"kim", whose paths the same trace also fits. Scored instead at a constant standing in for what "I'm" is actually worth, checked against the shipped dictionary and grid before shipping rather than guessed. Separately: dismiss now waits for the finger to sit still before a downward swipe counts, so a fast trace that happens to start moving down (tracing "no" or "on") no longer races the close gesture and loses depending on touch-sample timing.
-
v1.0.19 (2026-08-01) — Reverted an in-progress attempt at swipeable contractions that didn't land cleanly (see v1.0.21 above for the version that did).
-
v1.0.16 (2026-08-01) — The two-letter allowlist brought back in step with what the tests actually pin, plus "hi".
-
v1.0.13 (2026-07-31) — The commonest two-letter words are pinned by a test. "to", "it", "is" and "my" already decoded first after v1.0.11 — measured across twenty seeds, three trace speeds and the bare two-point flick the view reports for a quick stroke, every one of them wins outright. But the two-letter test measured a rate (22 of 26 first, all 26 reachable), and a rate would still pass with any single word broken, so those four now have their own assertion. No ranking change: nothing needed fixing. The count in the older test's comment was one high, and the fourth word that loses on frequency ("ah" to "an") was unnamed; both corrected.
-
v1.0.11 (2026-07-31) — Two-letter words can be swiped. "of", "to", "in", "is", "it" and "on" are all in the twenty commonest words in English, and none of them could be swiped at all: the decoder's dictionary scan began at three letters, so a stroke from o to f came out "off", t to o came out "top", b to y came out "buy". Between neighbouring keys it was worse — "we", "as", "ok" produced nothing whatsoever, because the whole stroke was shorter than the path length the decoder needed before it would treat a drag as a gesture, and the first letter had already been retracted by then. Two-letter words are now scored like any other, with two things keeping them honest: a stroke that begins and ends on the same key is still a tap (a drifted tap can never become a word), and a two-letter reading has to beat the field by a margin rather than tie it, because its ideal path is a straight line and a straight line fits almost anything. Scoring also gained a length term — a candidate is charged for distance the finger travelled that its own path can't account for, which is what tells "here" from "he" — and the trace now records where the finger lifted, so a quick flick between two keys isn't thrown away for having reported too few points. Single taps are untouched: "a" and "I" are still typed, never swiped.
-
v1.0.9 (2026-07-31) — Suggestions ranked against the sentence, and a strip you can edit. Three things:
- A bundled word-pair table (
res/raw/bigrams.bin, 180,000 pairs) reorders the candidates the strip, autocorrect and swipe typing already produced, using how much more often each pair occurs than chance. "happy bir" now offers birthday over birth, "went wro" wrong over wrote, "how mu" much over music. Reranking is bounded: a candidate climbs one place per two nats of pair strength, at most three places, and the candidate your spelling most obviously matches can never be pushed out of the strip. Autocorrect gets a far tighter bound again (1.2 places), because it commits without asking. Sentence starts have no pair to rank on and are left exactly as they were. If the table is missing or corrupt, ranking falls back to the old behaviour rather than failing. - Keeping the word you actually typed is always reachable. The keep-as-typed slot used to be suppressed while the dictionary could still extend what you'd typed, which meant a name that is also the start of an ordinary word (Wil, Kai, Cass, Ravi) never got a slot at all. It now appears for a deliberate mid-sentence capital, for a word you've just finished, and for a word autocorrect has already replaced — where tapping it puts your spelling back and learns it.
- Hold a suggestion to forget it. Removed from your own list and added to a forgotten list that the bundled dictionary is filtered against too, so a word the corpus keeps pushing at you can actually be got rid of. It stays a correctly spelled word — typing it deliberately is still left alone — and the "My words" screen lists what you've forgotten with a ✕ to restore it.
Costs about 955 KB of APK (the pair table, deflated), taking the download from 9.7 MB to 10.6 MB. A pair lookup measures ~10 ns and the whole reranking pass adds ~12 µs to a keystroke — next to nothing beside the dictionary scan that produced the candidates, and no measurable change to autocorrect at all (1.36 ms before, 1.37 ms after).
- A bundled word-pair table (
-
v1.0.6 (2026-07-29) — Suggestion strip's keep-as-typed slot, iOS style; suppressed while the typed prefix still has completions, so typing "keyboard" doesn't first offer to add "k", "ke", "key".
-
v1.0.4 (2026-07-29) — Three-slot suggestion strip (off by default) and the "My words" personal dictionary.
-
v1.0.3 (2026-07-29) — Swipe typing, bundled-dictionary autocorrect, Return key moved to the corner; CI switched to publishing a signed APK on every push to
main. -
v0.1.5 (2026-06-07) — Numbers layer stays active across field restarts; open keyboard settings from the system input-method settings screen.
-
v0.1.4 (2026-06-01) — Adjustable keyboard height, in-settings keyboard tester, cleaner swipe-to-dismiss; numbers layer opens automatically for number/phone/date fields; APK version derived from the release tag so Obtainium stops showing phantom updates.
-
v0.1.3 (2026-05-31) — Keyboard layouts (QWERTY/AZERTY/QWERTZ), compact mode, adaptive typing settings; scrollbar adjusted to match LightOS exactly.
-
v0.1.2 (2026-05-31) — LightOS-style line-based toggles, replacing Material switches.
-
v0.1.1 (2026-05-31) — Fixed a VOSK (voice dictation) minification bug.
-
v0.1.0 (2026-05-31) — Initial fork of adam-weber/light-keyboard.
This is an independent, open-source project, made for the Light Phone but not by Light. The original is adam-weber/light-keyboard.
MIT. Do what you like with it.
It's not Light, it's Bright.
26 open-source apps for the Light Phone III — camera, music, maps, messages, reading, transit, games. The phone has no app store, so they install by sideload: scan one code from brightmarket.gzl.dev and BrightMarket keeps them updated.
Roll · BrightNotebook · BrightControl · BrightWay · BrightChat · browse all 26 →
The Light Phone does not sponsor or endorse any of these. Built by Giovanni Lupo — if this one is useful to you, a ⭐ helps the next person find it.
These apps are free, open, and built on my own time. Sponsorship pays the bills that don't go away: build servers, test hardware, and the crash reporter that keeps them shipping. Donation or not my code is always free for the world to use.

