Releases: AdamLovattDevOps/better-handheld-keyboard
Release list
v1.0.5 — Upgrades that actually upgrade
Two bugs with the same shape: files that were only ever written on a fresh install, so
an upgrade left the system in a state the rest of the code didn't expect.
Fixed
- Opacity cycling stopped working after a re-install. The KWin script had two writers:
the installer copied a staticmain.jswithw.opacityhardcoded to0.72and no
var OPline, while the swap daemon generated a different one that had it. The opacity
key patchesvar OP, so against the static copy it silently patched nothing and every
reload snapped the keyboard back to0.72— the config recorded each step down, the
window ignored them. There is now one writer,bin/handheld-kbd-kwin-script, shared by
the installer, the daemon and the opacity key; the static copy is gone. The key
regenerates through it, falling back to patching either historical script shape. - New keys never reached existing installs. Layouts are only written when absent, so a
release could ship the code for a key while the user's layout had no button for it —
which is exactly what happened to v1.0.4's ✥. The installer now merges in any action key
(locale,hide,size,opacity,move) the release has and the layout lacks,
backing the file up first, and prunes settings that no longer do anything (dockand
dock_edges, from the superseded slot design). Idempotent, and it leaves your own
customisations alone.
v1.0.4 — Unlock, drag, lock
The v1.0.3 move key cycled through preset docking slots. It didn't work well, so this
replaces it with the obvious thing: unlock the keyboard, put it where you want, lock it.
Changed
- The move key (✥) is now a lock toggle. Press it and the keyboard unlocks: KWin
stops forcing its position and size, and a bar appears along the top — drag anywhere on
it to move the window, or use either end (⤡ / ⤢) to resize. Press again and it locks
exactly where you left it, saving the result asgeometryso a respawn or relogin comes
back to the same place.- Wayland clients can't place their own windows, so both gestures hand off to the
compositor (begin_move_drag/begin_resize_drag) — the same mechanism a titlebar
uses — and the position is read back from what KWin remembered. - Unlocking switches the rule to Remember (4) and locking switches it back to Force (2),
so nothing nudges the keyboard while it's locked. - It always starts locked; an unlocked keyboard that got respawned would drift.
- Desktop Mode only — in Game Mode there's no window to drag, and the key says so.
- Wayland clients can't place their own windows, so both gestures hand off to the
- Removed the docking-slot cycle,
dockanddock_edges.handle_height(default 30)
sets the drag bar's height.
The v1.0.3 resolution clamping stays: the configured geometry is still clamped to the
panel it lands on, so a saved position can't put the keyboard off-screen.
v1.0.3 — A move key
Added
- Move key (
kind: "move", ✥). Steps the keyboard through docking slots instead of
leaving it pinned wherever the KWin rule put it:- slot 0 is the configured
geometry, anchored to the internal panel — unchanged
default, so nothing moves unless you ask it to - then one slot per (display, edge) pair —
bottom,top,middleby default via
dock_edges— internal panel first, centred horizontally on that display - the slot list is resolved live from
kscreen-doctor, so plugging in a monitor adds
its slots without a restart, and unplugging wraps the selection back into range - it also forces the KWin rule to be rewritten even when the rect looks unchanged,
which is the way out of a position that has got stuck - the chosen slot persists to
config.jsonasdock, and the key's label shows the
edge — plus the display name once there's more than one to choose from
- slot 0 is the configured
Fixed
- Works on any panel resolution. Every rect — including the configured one — is now
clamped to the display it's going onto: sizes shrink to fit, then the position is pulled
back inside. The shipped geometry is sized for a 1280×800 Steam Deck, so on a 1280×720
panel it used to hang 78px off the bottom, and a partly-off-screen Wayland window still
takes taps while not being fully visible. No-op where it already fits, so the Deck and
the Legion Go 2 are unchanged. - The installer sizes for the panel it's on. On a fresh install,
geometryand
big_geometryare derived from the internal display (full width, 55% / 64% height,
bottom-docked) instead of assuming 1280×800, and the forced KWin rule is written from
that geometry rather than a hardcoded0,378 1280,422. Existing configs are left alone.
Changed
- Display enumeration moved into one
_outputs()helper (name, position, current-mode
size, internal flag), which_panel_origin()now uses too.
v1.0.2 — Predictive text and a bigger keyboard
Everything from v1.0.1 stays: the trigger detection fix, the fail-visible KWin rule, the
recovery script. This adds the features that had been living on my own Legion Go 2.
Added
- Predictive text. A row of tappable suggestions above the keys, backed by
handheld_kbd_predict.py. Learns the words you commit (predict_learn, stored in
~/.local/share/handheld-kbd/learned.json, never leaves the device) and blends that
with corpus frequencies built once by the newhandheld-kbd-build-dict. Tapping a
suggestion erases the partial word and types the full one as real keystrokes, so it
works in any application. Falls back to a plain keyboard if the engine or its data is
missing, and off entirely with"prediction": false. - Big mode. A
sizekey (⤢) toggles betweengeometryand the newbig_geometry,
stretching every key to fill the window and rewriting the KWin rule live — no relogin.
start_bigcomes up in it. - Opacity cycling. An
opacitykey (◐) steps throughopacity_stepsand persists
the choice, instead of editing JSON to see what's behind the keyboard. - Swipe typing. Drag across the letters to type a word (
handheld_kbd_swipe.py).
A drag only counts once it travelsswipe_min_travelkey-widths and crosses
swipe_min_keysdistinct letters, so ordinary taps are untouched. Runner-up decodings
appear in the suggestion row. - Two more ways to summon it.
gesture_summonshows the keyboard on a two-finger
swipe up from the bottom edge of the touchscreen;show_on_focusshows it whenever a
text field takes focus, via AT-SPI (handheld-kbd-focus-probehelps identify what the
bridge reports). Both show-only and both off by default. - Resume handling.
handheld-kbd-resume-watch.pylistens for logind's
PrepareForSleepand re-initialises the keyboard on wake, fixing a stale trigger or
uinput handle after sleep. Installed as an autostart entry. - Multi-display docking. The window anchors to the internal panel (
internal_output,
auto-detectingeDP*) so an external display doesn't drag the keyboard off-screen.
Changed
- The KWin script now also keeps the keyboard above fullscreen windows (temporarily
dropping the fullscreen window tokeepBelow, restored afterwards) and re-asserts
focus on the window you were actually typing into when the keyboard maps. full.jsongains the ◐ and ⤢ keys.
Prediction data (unigrams.txt, bigrams.txt, learned.json, raw/) is generated on
device and git-ignored — the repo ships the builder, not the data.
v1.0.1 — Legion Go 1 keyboard recovery
If you installed v1.0.0 on a Legion Go 1 — or on a Steam Deck or ROG Ally running Bazzite
or ChimeraOS — and lost your on-screen keyboard entirely, this release fixes it. Sorry
about that.
Recover an affected install
Double-click Recover My Keyboard.desktop, or run:
handheld-kbd-recoverRe-running ./install.sh repairs the same thing. Either route restores Steam's keyboard and
switches this one to mirror mode. handheld-kbd-recover --stock-only stands everything down
and hands the desktop back to Steam's keyboard.
What was broken
The installer chose its "seamless" trigger by grepping InputPlumber's default profile for
button: Keyboard. That profile is generic — it carries the mapping on every device — so
seamless mode was selected on hardware where no button can ever emit the event. The Legion
Go 1's InputPlumber driver declares Gamepad:Button:Keyboard but nothing physical sends it;
the Steam Deck and ROG Ally drivers don't reference it at all. The remapped button did
nothing, and because the KWin script still forced Steam's on-screen keyboard to
opacity = 0.0, both keyboards were gone.
Stock SteamOS on a Steam Deck was unaffected — it doesn't ship InputPlumber — as was the
Legion Go 2, which has a real keyboard button.
Fixed
- Seamless mode is selected by DMI product name (Legion Go 2) rather than by the generic
InputPlumber profile; every other device defaults to mirror mode, which needs no hardware
button - Steam's keyboard is only made transparent once this one is known to appear — mirror mode
qualifies inherently, seamless mode waits for proof the trigger fired. A dead trigger now
leaves you with the stock keyboard instead of nothing - The daemon falls back to mirror mode for the session if the InputPlumber remap fails
handheld-kbd-ip-remapverifies the profile actually loaded, warns when a device reports
no keyboard button, and fails loudly instead of silentlyHomeandEnddropped from the full layout — the arrow cluster andPgUp/PgDnalready
cover that navigation
Added
handheld-kbd-recoverand a double-clickableRecover My Keyboard.desktop, because typing
is not an option when you have no keyboard- This changelog, and a tag-driven release workflow
Thanks to the Legion Go 1 user who reported it and confirmed the fix.
v1.0.0
Real-keystroke on-screen keyboard for SteamOS / KDE Plasma handhelds (Desktop Mode).
Keys are injected through /dev/uinput as a virtual input device, so Ctrl, Alt, Super, F1-F12, Tab, Esc and the arrows reach any focused application — including Ctrl+C in a terminal.
Trigger — the hardware keyboard button. Default mode mirrors Steam's OSK visibility; "mirror": false instead remaps the button via InputPlumber to fire a DBus event, so no keystroke leaks to Steam or KDE.
Configuration — ~/.config/handheld-kbd/config.json: opacity (default 0.72), geometry, key sizes, theme, layout and locale. Layouts and locales are plain JSON beside it.
Layouts — full and compact, US and UK. The locale key switches KDE's XKB layout and re-skins the labels so printed and typed stay in sync.
Install — ./install.sh or double-click the desktop launcher, then log out and back in (the input group is read at session start).
Requires KDE Plasma 6 (Wayland), python3, python-gobject (GTK 3), python-evdev.