uconsole-mapper is an input daemon for uConsole. The voice PTT path targets FlashAI's current ASR API with a personal ASR Token, calls POST /api/asr/transcriptions, and supports a conservative tmux/Codex TUI learning shortcut through POST /api/asr/transcription-events/:requestId/finalize. It maps gamepad and mouse events to commands, text input, or virtual input events, and it can still handle keyboard interception in a legacy compatibility mode.
BTN_TRIGGER(right-sideX) managescodex-buddyand makes new windows fullscreenBTN_TOP(right-sideY) managesQuickTermBTN_THUMB(right-sideA) types提交giton short press- Desktop keybind integration keeps
RightShift+Cfor Chromium,RightShift+Dfor~/zDesktop,RightShift+Ffor the file manager, andRightShift+Vfor VS Code through generatedkeyd -> session launcherbindings, installsShift+Enterfor terminal-style multiline input, and mapsCtrl+Alt+Enterto maximize the focused window - Mouse
BTN_MIDDLEis remapped toBTN_LEFT
The project uses a "daemon + configuration" design, which makes it easier to add combo bindings or custom actions than continuing to stack more input-remapper rules.
uconsole_mapper.py: main programconfig.toml.example: example configurationdesktop-keybinds.toml.example: declarative desktop shortcut configgenerate_desktop_keybinds.py: generates directkeydlaunch bindings pluslabwcsnippets from the desktop shortcut configrun-or-raise-chromium.sh: raises an existing Chromium window or starts a new onerun-or-raise-filemanager.sh: raises an existing file manager window or starts a new onerun-or-raise-vscode.sh: raises an existing VS Code window or starts a new onerun-or-raise-zdesktop.sh: opens~/zDesktopin the file manager or focuses that window if it already existsuconsole-launch-in-session.sh: runs GUI commands fromkeydinside the active desktop user sessionsync_labwc_keybinds.py: installs compositor-side keyboard shortcuts intolabwcsync_keyd_default_conf.py: detects the uConsole keyboard id and writes a device-specific/etc/keyd/default.confuconsole-mapper.service:systemd --userservice file99-uinput.rules: grants/dev/uinputaccess to theinputgroupinstall.sh: installation script
Sync this repository to the uConsole, then run:
sudo apt update
sudo apt install -y python3-evdev wtype wl-clipboard curl jq
sudo modprobe uinput
cd ~/WorkSpace/uconsole-mapper
./install.sh./install.sh marks python3-evdev as a manual package so a later
apt autoremove does not remove the mapper's core runtime dependency.
RightShift+C now depends on keyd. If your distro packages it, install
keyd before running ./install.sh; the installer will detect the current
uConsole keyboard id and wire /etc/keyd/default.conf automatically. The
installer also removes fcitx5's single-Shift toggle when it finds
Shift_L, because that hotkey conflicts with RightShift+... desktop binds.
The generated desktop launcher config lives at:
~/.config/uconsole-mapper/desktop-keybinds.tomlDefault configuration file path:
~/.config/uconsole-mapper/config.tomlDefault configuration example:
[general]
rescan_seconds = 3.0
session_watch_processes = ["wf-panel-pi", "labwc"]
session_watch_settle_ms = 1500
[gamepad]
device_name_patterns = ["ClockworkPI uConsole"]
debounce_ms = 250
[[gamepad.bindings]]
buttons = ["BTN_TRIGGER"]
command = "~/.local/bin/toggle-codex-buddy"
[[gamepad.bindings]]
buttons = ["BTN_TOP"]
command = "~/.local/bin/toggle-lxterminal"
[[gamepad.bindings]]
buttons = ["BTN_THUMB"]
command = "printf '%s' '提交git' | wl-copy && wtype -M ctrl -k v -m ctrl"
# Push-to-talk voice input.
# Replace BTN_THUMB2 with the actual B key code on your device if needed.
# [[gamepad.bindings]]
# buttons = ["BTN_THUMB2"]
# press_command = "~/.local/bin/uconsole-voice-ptt start"
# release_command = "~/.local/bin/uconsole-voice-ptt stop"
[keyboard]
enabled = false
grab = false
device_name_patterns = ["ClockworkPI uConsole Keyboard"]
debounce_ms = 250
[mouse]
enabled = true
grab = true
device_name_patterns = []
[[mouse.remaps]]
from = "BTN_MIDDLE"
to = "BTN_LEFT"systemctl --user status uconsole-mapper.service
journalctl --user -u uconsole-mapper.service -fCurrent toggle-codex-buddy behavior:
- No
codex-buddywindow: opens a newcodex-buddy uConsolewindow and switches it to fullscreen - A
codex-buddywindow exists but is not focused: raises it to the foreground codex-buddyis already focused: minimizes or hides itcodex-buddyloses focus: minimizes automatically so it does not remain behind other overlay windows
Current toggle-lxterminal behavior:
- No
QuickTermwindow: opens a newlxterminal --title=QuickTermin its own user scope - A
QuickTermwindow exists but is not focused: raises it to the foreground QuickTermis already focused: minimizes or hides it- Rapid duplicate button events are ignored to avoid toggling the window twice from one press
If the service does not start, check these first:
- Whether
python3-evdevis installed - Whether
python3-evdevwas removed byapt autoremove - Whether
wtypeis installed when text-input bindings are used - Whether
wl-copyfromwl-clipboardis installed when paste-style bindings are used - Whether
/dev/uinputexists - Whether the current user has read access to
/dev/input/event* - Whether
~/.local/bin/toggle-lxterminalis executable - Whether the Wayland session is
wayland-0
gamepad.bindings also supports these fields:
hold_ms: delay before triggering while held, in milliseconds; default0repeat_ms: repeat interval while held after triggering, in milliseconds; default0text: types a string throughwtypepress_enter: sends Enter aftertext; defaultfalsepress_command: runs once when the combo becomes activerelease_command: runs once when the combo becomes inactive; withhold_ms, it only runs after the hold threshold is reached
When a normal gamepad.bindings action uses the same buttons as a hold_ms
binding, the normal action is treated as a short press and runs on release only
if the hold action did not fire.
press_command / release_command are intended for push-to-talk style actions
where press starts immediately and release stops. With hold_ms, press still
runs immediately, but release only runs after the hold threshold is reached.
This keeps long-press recording responsive while allowing the same button to use
a normal command as a short-press action. Phase bindings cannot be combined
with repeat_ms, text, or emit_*.
keyboard.bindings also supports these fields:
repeat_rate: virtual keyboard repeat rate in keys per second; default30repeat_delay_ms: virtual keyboard repeat delay in milliseconds; default300emit_rel+emit_rel_value: sends relative mouse events such as wheel scrollingrepeat_ms: repeat interval while held after triggering, in milliseconds; default0text: types a string throughwtypepress_enter: sends Enter aftertext; defaultfalse
The included shift-enter-newline helper translates Shift+Enter into Ctrl+J
when QuickTerm is focused. This matches Codex CLI multiline input behavior in
terminal UIs, where plain Shift+Enter is often not exposed as a distinct key.
By default, keyboard shortcuts are no longer routed through uconsole-mapper.
Normal typing stays on the physical keyboard path. RightShift+... launchers
are handled directly by keyd, while compositor-specific binds stay in
labwc.
Default declaration:
[[rightshift.bindings]]
key = "c"
command = "~/.local/bin/run-or-raise-chromium"
[[rightshift.bindings]]
key = "f"
command = "~/.local/bin/run-or-raise-filemanager"
[[rightshift.bindings]]
key = "d"
command = "~/.local/bin/run-or-raise-zdesktop"
[[rightshift.bindings]]
key = "v"
command = "~/.local/bin/run-or-raise-vscode"
[[labwc.bindings]]
key = "S-Return"
command = "~/.local/bin/shift-enter-newline"
[[labwc.bindings]]
key = "C-A-Return"
action = "Maximize"
[[labwc.bindings]]
key = "C-A-d"
command = "~/.local/bin/uconsole-show-desktop"Generated default behavior:
RightShift+C:keydruns/usr/local/bin/uconsole-launch-in-session ~/.local/bin/run-or-raise-chromiumRightShift+D:keydruns/usr/local/bin/uconsole-launch-in-session ~/.local/bin/run-or-raise-zdesktopRightShift+F:keydruns/usr/local/bin/uconsole-launch-in-session ~/.local/bin/run-or-raise-filemanagerRightShift+V:keydruns/usr/local/bin/uconsole-launch-in-session ~/.local/bin/run-or-raise-vscodeShift+Enter:labwcruns~/.local/bin/shift-enter-newlineCtrl+Alt+Enter:labwcmaximizes the focused windowCtrl+Alt+D:labwcruns~/.local/bin/uconsole-show-desktop
install.sh installs these pieces:
~/.config/uconsole-mapper/desktop-keybinds.toml: the declaration you maintain~/.local/share/uconsole-mapper/keyd-uconsole-mapper: generatedkeydsnippet~/.local/share/uconsole-mapper/labwc-keybinds.xml: generatedlabwcblock/usr/local/bin/uconsole-launch-in-session: helper that re-enters the active user session fromkeyd~/.config/labwc/rc.xml:sync_labwc_keybinds.pyinserts the generated block/etc/keyd/uconsole-mapper: installed copy of the generatedkeydsnippet/etc/keyd/default.conf:sync_keyd_default_conf.pywrites explicit uConsole keyboard ids plusinclude uconsole-mapper
This split is intentional. keyd can reliably match a right-only Shift combo,
while labwc remains the right place for compositor-facing binds such as
Shift+Enter. Launching Chromium directly from keyd avoids the previous
bridge-key path, which interacted badly with fcitx5 single-Shift toggles.
The installer now writes explicit device ids into /etc/keyd/default.conf
instead of relying on the * wildcard. This is important on uConsole because
keyd may otherwise ignore the built-in keyboard entirely.
If you use fcitx5, avoid binding plain Shift_L as an input-method toggle.
That hotkey can steal or distort RightShift+... launchers because keyd
needs Shift-layer semantics to distinguish side-specific combos.
Legacy [keyboard] interception mode still exists for compatibility, but it is
disabled in the example config because it puts normal typing on top of the
mapper's grab-and-reemit path.
Current right-side button mapping on uConsole:
BTN_TRIGGER=XBTN_THUMB=ABTN_TOP=Y
The repository includes a standalone script, uconsole-voice-ptt, intended to be called from uconsole-mapper via press_command / release_command:
[[gamepad.bindings]]
buttons = ["BTN_THUMB2"]
command = "~/.local/bin/uconsole-voice-ptt learn"
[[gamepad.bindings]]
buttons = ["BTN_THUMB2"]
hold_ms = 600
press_command = "~/.local/bin/uconsole-voice-ptt start"
release_command = "~/.local/bin/uconsole-voice-ptt stop"Default configuration files:
~/.config/uconsole-mapper/voice.env
~/.config/uconsole-mapper/voice-glossary.txtvoice-glossary.txt accepts one term per line. Blank lines and lines starting with # are ignored. The default example includes git.
Example voice.env:
WHISPER_URL=http://127.0.0.1:3300/api/asr/transcriptions
WHISPER_MODEL=faster-whisper-small
WHISPER_LANGUAGE=zh
WHISPER_AUTH_TOKEN=fa_asr_replace_me
WHISPER_CORRECTION_MODE=auto
VOICE_OUTPUT_MODE=paste
VOICE_TMUX_OUTPUT_MODE=type
VOICE_WECHAT_OUTPUT_MODE=paste
VOICE_TYPE_BACKEND=auto
VOICE_TMUX_TYPE_BACKEND=wtype
VOICE_PASTE_BACKEND=uinput
VOICE_PASTE_SHORTCUT=ctrl_v
VOICE_WECHAT_PASTE_SHORTCUT=ctrl_v
VOICE_NOTIFY_USE_MARKUP=0
VOICE_NOTIFY_FONT_SIZE=22
VOICE_NOTIFY_PADDING_LINES=1
VOICE_MAX_RECORD_MS=60000Optional ASR request variables:
# FlashAI personal ASR Token. Required for transcription and the tmux/Codex learning shortcut.
# WHISPER_AUTH_TOKEN=fa_asr_xxx
# Optional explicit finalize URL. Defaults from WHISPER_URL.
# WHISPER_FINALIZE_URL=http://127.0.0.1:3300/api/asr/transcription-events/{requestId}/finalize
# Optional short ASR hint sent to the upstream transcription model.
# WHISPER_PROMPT=
# Multipart field name for the ASR prompt. Defaults to prompt.
# WHISPER_PROMPT_FIELD=prompt
# Multipart field name for request-level glossary JSON. Defaults to promptGlossary.
# WHISPER_PROMPT_GLOSSARY_FIELD=promptGlossary
# User glossary file, one term per line.
# VOICE_GLOSSARY_FILE=~/.config/uconsole-mapper/voice-glossary.txt
# Multipart field name for tmux context sent to correction. Defaults to contextText.
# WHISPER_CONTEXT_FIELD=contextText
# ASR correction mode: off | on | auto. Auto keeps normal text fast and corrects code/command mixed input.
# WHISPER_CORRECTION_MODE=auto
# Legacy compatibility only; prefer WHISPER_CORRECTION_MODE.
# WHISPER_ENABLE_CORRECTION=0
# ASR request timeout in seconds. Defaults to 60; use 0 to disable.
# WHISPER_TIMEOUT=60
# Maximum recording duration before silently starting a new recording segment. Defaults to 60000 ms; use 0 to disable.
# VOICE_MAX_RECORD_MS=60000
# Include the current tmux active pane visible text when a tmux terminal is focused.
VOICE_TMUX_CONTEXT=1
# If the visible area is too short, fall back to at least this many recent lines.
# VOICE_TMUX_CONTEXT_LINES=30
# VOICE_TMUX_CONTEXT_MAX_CHARS=1200
# Short-press B correction dialog guardrails. Corrections too different from the last ASR text are rejected.
# VOICE_LEARN_MAX_AGE_SECONDS=600
# VOICE_LEARN_MAX_EDIT_RATIO=0.38
# VOICE_LEARN_REPLACE_INPUT=1
# VOICE_LEARN_REPLACE_MAX_CHARS=300
# VOICE_LEARN_DIALOG_FONT_SIZE=22
# VOICE_LEARN_DIALOG_WIDTH=820
# VOICE_LEARN_DIALOG_HEIGHT=220Script behavior:
start: starts recordingstop: stops recording, uploads the audio to FlashAI ASR, retrieves the transcript, injects it into the currently focused input field, and stores the last ASR state for later correctionlearn: short-press correction action. It opens an edit dialog prefilled with the last ASR text; pressing Enter submits the edited text to FlashAI finalize and, by default, replaces the previously inserted ASR text in the active inputcancel: stops the active recording and deletes the audio without sending it to ASR- if a tmux terminal is available, the script captures the current active tmux pane visible text as context for ASR correction; learning itself no longer reverse-extracts text from tmux output
- learning is intentionally conservative: if the state expired, the edited text is empty, unchanged, too different, or the replacement would delete too many characters, the script refuses to learn or skips input replacement
Supported output modes:
type: types the text directly throughwtypetype_enter: types the text and then presses Enterclipboard: writes only to the clipboardpaste: writes to the clipboard first, then simulates a configurable paste shortcutfcitx_commit: writes the text to a pending file and asks anfcitx5-luabridge to commit it into the currently focused input context
Window-specific behavior:
- non-tmux windows use
VOICE_OUTPUT_MODE;pasteis the default because more desktop apps accept clipboard paste than synthetic typing or fcitx quick-phrase commits - tmux / QuickTerm use
VOICE_TMUX_OUTPUT_MODEwhenever the terminal window is focused; the default istype, so shell apps still receive direct text input even if tmux context capture is unavailable - WeChat windows use
VOICE_WECHAT_OUTPUT_MODE; the default ispastebecause WeChat may not accept the fcitx quick-phrase commit path reliably VOICE_TYPE_BACKEND=autoprefersydotoolwhen its socket is available, otherwise falls back towtypeVOICE_TMUX_TYPE_BACKENDdefaults towtype, so tmux keeps the previous direct-text path unless you explicitly change itVOICE_PASTE_SHORTCUTdefaults toctrl_v, which is accepted by WeChat and most desktop appsVOICE_WECHAT_PASTE_SHORTCUTdefaults toctrl_v, so WeChat can stay onCtrl+Veven if the global paste shortcut is changedVOICE_PASTE_BACKEND=autoprefers theuconsole-pasteuinput helper when it is installed, otherwise falls back towtype; useuinputto require the helper- this also avoids the Chromium-side issue where direct virtual-keyboard typing can land as key positions such as
1234567890
Notification behavior:
- while recording / transcribing, the script updates a single persistent status notification
- after successful transcription injection, the notification is closed instead of showing the recognized text
VOICE_NOTIFY_USE_MARKUPdefaults to0; enable it only if the notification daemon correctly renders Pango markupVOICE_NOTIFY_FONT_SIZEdefaults to22and only applies when markup is enabledVOICE_NOTIFY_PADDING_LINESdefaults to1to make the notification taller
If the B button on the device is not BTN_THUMB2, check the service logs or temporarily run evtest / libinput debug-events to confirm the actual key code before updating the configuration.
uconsole-mapper.serviceusesRestart=always, so systemd restarts the daemon if the main process exits- if a keyboard watcher task dies unexpectedly, the daemon now treats that as fatal and lets systemd restart the whole service immediately
- if keyboard grab is enabled but the virtual keyboard write path breaks at runtime, the keyboard watcher drops out of grab mode and falls back to direct passthrough instead of keeping the physical keyboard locked
- by default the daemon also watches
wf-panel-piandlabwc; if either process restarts and the PID change stays stable for1500ms, the daemon exits so systemd can recreate the virtual keyboard path cleanly - the default desktop shortcut path no longer depends on
uconsole-mapper, so taskbar or compositor churn no longer takes normal typing orRightShift+Cdown with it