This guide explains how to configure your TourBox device and customize button mappings.
TuxBox supports two configuration formats:
Profiles are stored as individual .profile files in ~/.config/tuxbox/profiles/:
~/.config/tuxbox/
├── config.conf # Device and service settings
└── profiles/
├── default.profile # Default profile (required)
├── davinci_resolve.profile
├── blender.profile
└── ...
Benefits:
- Easy to share profiles with other users
- Import/export profiles via the GUI
- Better organization for many profiles
All profiles in one file at ~/.config/tuxbox/mappings.conf:
[device]
mac_address = XX:XX:XX:XX:XX:XX
[profile:default]
side = KEY_A
...
[profile:VSCode]
app_id = Code
...Note: The GUI will offer to migrate your configuration to the new format on first launch. Your original config is backed up before migration.
Note: If you used
install.shfrom the README, you already have a config file set up with your MAC address. You can skip to step 2 below to edit your configuration.
Only run this if:
- You didn't use the main
install.shscript - You want to reset your config back to defaults (WARNING: loses all customizations!)
- You want to get updated example profiles
./install_config.shThis creates ~/.config/tuxbox/mappings.conf. If you don't have a MAC address configured yet, it will prompt you to enter it, otherwise it will preserve it.
WARNING: Running this on an existing config will:
- Replace ALL your custom button mappings with defaults
- Replace ALL your custom profiles with example profiles
- Preserve ONLY your MAC address (everything else is lost)
nano ~/.config/tuxbox/mappings.conf
# or
gedit ~/.config/tuxbox/mappings.conf# If MAC address is in config file:
sudo ./venv/bin/python -m tuxbox.device_ble
# Or override with command line:
sudo ./venv/bin/python -m tuxbox.device_ble D9:BE:1E:CC:40:D7This config system uses profiles to define button mappings.
- All configs use
[profile:name]sections - The
[profile:default]section is required and serves as the fallback - On X11: Only
[profile:default]is used (no automatic switching) - On Wayland: Profiles automatically switch based on focused window
This means:
- X11 users simply configure
[profile:default]and ignore app-specific profiles - Wayland users can add app-specific profiles for automatic switching
The config file uses INI format with these sections:
Configures your TourBox device connection.
Format: setting = value
Available settings:
mac_address- Your TourBox's Bluetooth MAC address (XX:XX:XX:XX:XX:XX) - Elite/Elite Plus onlymodifier_delay- Milliseconds to wait between modifier keys (Ctrl/Shift/Alt/Meta) and other keyboard keys when sending key combinations. Default:0(disabled). Set to20-50if apps like GIMP don't recognize keyboard combos. Can be overridden per-profile in.profilefiles (see below). Note: modifier+mouse combos (e.g. Alt+scroll, Ctrl+click) get a small built-in delay automatically and do not require this setting.
How to find your MAC address:
bluetoothctl devicesLook for "TourBox Elite" or "TourBox Elite Plus" in the output.
Example:
[device]
mac_address = D9:BE:1E:CC:40:D7
modifier_delay = 0Configures how the GUI manages the driver service. This is primarily useful for systems without systemd (OpenRC, runit, s6, etc.).
Available settings:
restart_command- Custom command to restart the driver service
Example for OpenRC (Gentoo):
[service]
restart_command = rc-service tuxbox restartExample for runit:
[service]
restart_command = sv restart tuxboxExample for s6:
[service]
restart_command = s6-svc -r /run/service/tuxboxNotes:
- If not configured, the GUI will use systemctl (if available)
- The restart command should work whether the service is running or stopped
- Saving profiles does NOT require this setting (reload uses direct signal)
- Only "File -> Restart Driver" in the GUI uses this command
Profiles define button and rotary control mappings. You can have multiple profiles.
The [profile:default] section is REQUIRED - it's used:
- On X11: Always (no profile switching)
- On Wayland: When no app-specific profile matches
Format:
[profile:name]
# Optional: Window matchers (for app-specific profiles on Wayland)
window_class = ApplicationClass
window_title = Window Title
app_id = app.id
# Button mappings
side = KEY_A
top = KEY_B
# ... (all buttons and rotary controls)
# Rotary mappings
scroll_up = REL_WHEEL:1
scroll_down = REL_WHEEL:-1
# ... (all rotary controls)Available buttons:
side,top,tall,short- Main buttonsc1,c2- Right side buttonsdpad_up,dpad_down,dpad_left,dpad_right- D-padscroll_click,knob_click,dial_click- Clickable controlstour- Tour button
Available rotary controls:
scroll_up,scroll_down- Scroll wheelknob_cw,knob_ccw- Knob (clockwise/counter-clockwise)dial_cw,dial_ccw- Dial (clockwise/counter-clockwise)
Window Matchers:
You can use one or more of these matchers in a profile. If you specify multiple matchers, the profile matches if any of them match (OR logic).
-
window_class- Exact match, case-insensitive- Must match the entire class name
- Example:
window_class = Codematches class "Code" or "code", but NOT "VSCode"
-
app_id- Exact match, case-insensitive- Must match the entire app_id
- Example:
app_id = firefox-esrmatches "firefox-esr" or "Firefox-ESR", but NOT "firefox"
-
window_title- Substring match, case-insensitive- Matches if the matcher appears anywhere in the title
- Example:
window_title = Firefoxmatches "Mozilla Firefox", "GitHub - Mozilla Firefox", etc. - Most flexible option for matching windows
Matching Examples:
# Match by exact class
[profile:vscode]
window_class = Code
# Match by partial title (more flexible)
[profile:browser]
window_title = Firefox
# Use multiple matchers - matches if ANY match
[profile:editor]
window_class = Code
app_id = code
window_title = Visual StudioFinding window_class for your applications:
Sway:
swaymsg -t get_tree | grep app_id
# Focus the window, then look for "app_id": "..."Hyprland:
hyprctl activewindow | grep class
# Focus the window, shows current window classTest script (any compositor):
The window monitor tool will show you the window class, app_id, and title for any window you focus. This is the easiest way to find the correct values for your config file.
To run it:
# Navigate to the tuxbox directory
cd /path/to/tuxbox
# Run the window monitor
./venv/bin/python -m tuxbox.window_monitorThe monitor will continuously display information about the currently focused window. As you switch between different applications, you'll see output like:
Active window: WindowInfo(app_id='firefox-esr', title='Mozilla Firefox', class='firefox-esr')
Active window: WindowInfo(app_id='code', title='README.md - Visual Studio Code', class='Code')
Use the class value in your window_class field or the app_id value in your app_id field in the config file. Press Ctrl+C to stop the monitor when you're done.
Example profile configuration:
[device]
mac_address = D9:BE:1E:CC:40:D7
# Default profile (fallback)
[profile:default]
side = KEY_LEFTMETA
top = KEY_LEFTSHIFT
scroll_up = REL_WHEEL:1
scroll_down = REL_WHEEL:-1
# VSCode profile
[profile:vscode]
window_class = Code
side = KEY_LEFTCTRL+KEY_SPACE # Code completion
top = KEY_LEFTCTRL+KEY_SLASH # Toggle comment
c1 = KEY_LEFTCTRL+KEY_Z # Undo
c2 = KEY_LEFTCTRL+KEY_Y # Redo
scroll_up = REL_WHEEL:1
scroll_down = REL_WHEEL:-1
# Firefox profile
[profile:firefox]
window_class = firefox
side = KEY_LEFTALT+KEY_LEFT # Back
top = KEY_LEFTALT+KEY_RIGHT # Forward
c1 = KEY_LEFTCTRL+KEY_W # Close tab
c2 = KEY_LEFTCTRL+KEY_T # New tab
scroll_up = REL_WHEEL:1
scroll_down = REL_WHEEL:-1How it works:
- Driver starts with
defaultprofile - Window monitor checks focused window every 200ms
- When window changes, driver looks for matching profile
- If match found, instantly switches button mappings
- Console shows:
Switched to profile: vscode - If no match, falls back to
defaultprofile
Notes:
- Profile switching is instant - no reconnection needed
- Only works on Wayland (X11 support possible in future)
- All profiles must have complete button/rotary definitions
- Profiles can have different capabilities (e.g., one uses REL_WHEEL, another uses keys)
side = KEY_A
top = KEY_SPACEc1 = KEY_LEFTCTRL+KEY_C # Ctrl+C (copy)
c2 = KEY_LEFTCTRL+KEY_V # Ctrl+V (paste)c1 = KEY_LEFTCTRL+KEY_LEFTSHIFT+KEY_T # Ctrl+Shift+Tscroll_up = REL_WHEEL:1 # Scroll up
scroll_down = REL_WHEEL:-1 # Scroll down
dial_cw = REL_HWHEEL:1 # Horizontal scroll right
dial_ccw = REL_HWHEEL:-1 # Horizontal scroll leftknob_click = none # Disable this buttonThe TourBox Elite and Elite Plus have haptic motors that provide vibration feedback when rotating the knob, scroll wheel, or dial. You can configure both haptic strength (vibration intensity) and speed (detent spacing) in the config file.
Note: Haptic feedback is only available on the TourBox Elite and Elite Plus. The TourBox Neo does not have haptic motors.
Set haptic strength and speed for all rotary controls in a profile:
[profile:default]
haptic = strong # Strength: off, weak, strong
haptic_speed = fast # Speed: fast, medium, slowOverride the global settings for specific dials:
[profile:default]
haptic = weak # Global strength
haptic_speed = fast # Global speed
haptic.knob = strong # Override strength for knob
haptic_speed.knob = slow # Override speed for knob
haptic.scroll = off # Override strength for scroll wheel
haptic_speed.scroll = medium # Override speed for scroll wheelSet haptic strength and speed for specific modifier+dial combinations:
[profile:default]
haptic = weak # Global strength
haptic_speed = fast # Global speed
haptic.knob = strong # Knob alone uses strong
haptic.knob.side = off # side + knob uses no haptic
haptic_speed.knob.side = slow # side + knob uses slow speed
haptic.scroll.tall = strong # tall + scroll uses strong
haptic_speed.scroll.tall = medium # tall + scroll uses medium speedWhen determining haptic settings for a rotary event, the driver checks in this order (same for both strength and speed):
- Per-combo setting (
haptic.dial.modifier/haptic_speed.dial.modifier) - Most specific - Per-dial setting (
haptic.dial/haptic_speed.dial) - Dial-specific override - Global setting (
haptic/haptic_speed) - Profile-wide default - Default - Off for strength, Fast for speed (if nothing configured)
| Value | Description |
|---|---|
off |
No vibration feedback |
weak |
Subtle vibration |
strong |
Pronounced vibration |
| Value | Description |
|---|---|
fast |
More detents per rotation, finer control |
medium |
Balanced detent spacing |
slow |
Fewer detents per rotation, coarser control |
[profile:photo_editing]
window_class = gimp
haptic = strong # Default: strong feedback
haptic_speed = fast # Default: fine control
# Override for specific use cases
haptic.dial = weak # Dial for fine adjustments
haptic_speed.dial = slow # Fewer detents for sweeping movements
haptic.knob.tall = off # No haptic when using tall+knob
haptic_speed.knob.tall = medium # Medium speed for tall+knob combo
# Button mappings...
scroll_up = KEY_LEFTCTRL+KEY_EQUAL
scroll_down = KEY_LEFTCTRL+KEY_MINUSSome applications (notably GIMP) don't recognize keyboard combinations when the modifier key and the main key arrive at nearly the same time. The modifier_delay setting adds a small pause between them.
Mouse combos are handled automatically. When a binding mixes a modifier (Ctrl/Shift/Alt/Meta) with a mouse event (
REL_WHEEL,REL_HWHEEL,BTN_LEFT,BTN_RIGHT,BTN_MIDDLE), TuxBox always inserts a small delay between the modifier press and the mouse event so the compositor has time to latch the modifier into keystate. You do not need to setmodifier_delayfor things likeKEY_LEFTALT+REL_WHEEL:1(Alt+scroll) orKEY_LEFTCTRL+BTN_LEFT(Ctrl+click). If you have setmodifier_delayto a value larger than the built-in floor, your value wins.
Set in config.conf to apply to all profiles:
[device]
modifier_delay = 30Override the global setting for a specific profile by adding modifier_delay to the [profile] section of a .profile file:
[profile]
name = Gimp
app_id = gimp
modifier_delay = 30Priority: Per-profile value > Global [device] value > 0 (disabled)
- Omitted from profile: Uses the global setting
- Set to
0in profile: Explicitly disables delay for this profile, even if global is non-zero - Set to
30in profile: Uses 30ms delay regardless of global setting
Recommended values: 20-50ms. Start with 30 and adjust if needed.
KEY_LEFTCTRL, KEY_RIGHTCTRL
KEY_LEFTSHIFT, KEY_RIGHTSHIFT
KEY_LEFTALT, KEY_RIGHTALT
KEY_LEFTMETA (Super/Windows key)
KEY_A, KEY_B, KEY_C, ... KEY_Z
KEY_0, KEY_1, KEY_2, ... KEY_9
KEY_F1, KEY_F2, ... KEY_F12
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT
KEY_HOME, KEY_END
KEY_PAGEUP, KEY_PAGEDOWN
KEY_SPACE, KEY_ENTER, KEY_TAB, KEY_ESC
KEY_BACKSPACE, KEY_DELETE, KEY_INSERT
KEY_CONTEXT_MENU (right-click menu)
KEY_ZOOMRESET, KEY_ZOOMIN, KEY_ZOOMOUT
REL_WHEEL:1 # Scroll up
REL_WHEEL:-1 # Scroll down
REL_HWHEEL:1 # Scroll right
REL_HWHEEL:-1 # Scroll left
All examples use the profile format. Simply copy the button mappings into your [profile:default] section, or create app-specific profiles on Wayland.
[profile:gimp]
window_class = gimp
side = KEY_LEFTCTRL+KEY_Z # Undo
top = KEY_LEFTCTRL+KEY_Y # Redo
tall = KEY_B # Brush tool
short = KEY_SPACE # Pan (hold space + drag)
c1 = KEY_LEFTBRACE # Decrease brush size
c2 = KEY_RIGHTBRACE # Increase brush size
scroll_up = REL_WHEEL:1 # Zoom in
scroll_down = REL_WHEEL:-1 # Zoom out
knob_cw = KEY_RIGHTBRACE # Brush size up
knob_ccw = KEY_LEFTBRACE # Brush size down
# ... (add remaining buttons)[profile:kdenlive]
window_class = kdenlive
side = KEY_SPACE # Play/Pause
top = KEY_LEFTCTRL+KEY_Z # Undo
c1 = KEY_I # Mark in
c2 = KEY_O # Mark out
dpad_left = KEY_LEFT # Previous frame
dpad_right = KEY_RIGHT # Next frame
scroll_up = KEY_UP # Previous track
scroll_down = KEY_DOWN # Next track
knob_cw = KEY_EQUAL # Zoom timeline in
knob_ccw = KEY_MINUS # Zoom timeline out
# ... (add remaining buttons)[profile:blender]
window_class = blender
side = KEY_G # Grab/Move
top = KEY_R # Rotate
tall = KEY_S # Scale
short = KEY_SPACE # Search
c1 = KEY_LEFTCTRL+KEY_Z # Undo
c2 = KEY_LEFTCTRL+KEY_LEFTSHIFT+KEY_Z # Redo
scroll_up = REL_WHEEL:1 # Zoom
scroll_down = REL_WHEEL:-1 # Zoom
knob_cw = KEY_PERIOD # Rotate view right
knob_ccw = KEY_COMMA # Rotate view left
# ... (add remaining buttons)[profile:vscode]
window_class = Code
side = KEY_LEFTCTRL+KEY_SPACE # Code completion
top = KEY_LEFTCTRL+KEY_SLASH # Toggle comment
c1 = KEY_LEFTCTRL+KEY_Z # Undo
c2 = KEY_LEFTCTRL+KEY_Y # Redo
tall = KEY_LEFTALT+KEY_ENTER # Quick fix
short = KEY_F2 # Rename
scroll_up = KEY_LEFTCTRL+KEY_UP # Scroll up
scroll_down = KEY_LEFTCTRL+KEY_DOWN # Scroll down
knob_cw = KEY_LEFTCTRL+KEY_EQUAL # Increase font size
knob_ccw = KEY_LEFTCTRL+KEY_MINUS # Decrease font size
# ... (add remaining buttons)[profile:firefox]
window_class = firefox
side = KEY_LEFTALT+KEY_LEFT # Back
top = KEY_LEFTALT+KEY_RIGHT # Forward
c1 = KEY_LEFTCTRL+KEY_T # New tab
c2 = KEY_LEFTCTRL+KEY_W # Close tab
tall = KEY_LEFTCTRL+KEY_LEFTSHIFT+KEY_T # Reopen closed tab
short = KEY_F5 # Refresh
scroll_up = REL_WHEEL:1 # Scroll up
scroll_down = REL_WHEEL:-1 # Scroll down
knob_cw = KEY_LEFTCTRL+KEY_TAB # Next tab
knob_ccw = KEY_LEFTCTRL+KEY_LEFTSHIFT+KEY_TAB # Previous tab
# ... (add remaining buttons)The driver searches for config files in this order:
- Custom path (via
-coption) ~/.config/tuxbox/mappings.conf(user config)/etc/tuxbox/mappings.conf(system-wide)
The driver looks for the MAC address in this order:
- Command line argument -
python -m tuxbox.device_ble D9:BE:1E:CC:40:D7 - Environment variable -
TOURBOX_MAC=D9:BE:1E:CC:40:D7 python -m tuxbox.device_ble - Config file -
mac_addressin[device]section
This allows you to:
- Store MAC in config for convenience
- Override with command line for testing multiple devices
- Use environment variables in scripts/systemd
Before testing manually, stop the systemd service (if running):
systemctl --user stop tuxboxcd /path/to/tuxbox
./venv/bin/python -m tuxbox.device_ble -vThe driver will read your MAC address from ~/.config/tuxbox/mappings.conf.
./venv/bin/python -m tuxbox.device_ble -c my_custom_config.confWhen done testing, restart the service:
systemctl --user start tuxbox- Start the driver
- Open a text editor (gedit, nano, vim, VSCode)
- Press each button and verify the expected action happens
- Check the config file syntax
- Verify the key name is correct (case-sensitive!)
- Check logs to see if button is detected:
# If running as systemd service: journalctl --user -u tuxbox -f # Or run manually with verbose mode: systemctl --user stop tuxbox cd /path/to/tuxbox ./venv/bin/python -m tuxbox.device_ble -v
- Look for typos in key combinations
- Check if the
+separator is used correctly - Verify you restarted the driver after editing
- Key name must match exactly (e.g.,
KEY_A, notKEY_a) - Check the "Available Key Names" section above
- See full list:
python3 -c "from evdev import ecodes; print([k for k in dir(ecodes) if k.startswith('KEY_')])"
- Check file permissions:
ls -l ~/.config/tuxbox/mappings.conf - Verify INI syntax (no extra quotes, proper
=signs) - Run with
-vto see which config file is loaded
Check compositor detection:
python3 -m tuxbox.window_monitor
# Should show your compositor and window changesIf no compositor detected:
- Verify you're on Wayland:
echo $XDG_SESSION_TYPE(should say "wayland") - Check compositor-specific tool is installed:
- Sway:
which swaymsg - Hyprland:
which hyprctl - GNOME:
which gdbus - KDE:
which qdbus
- Sway:
Check window_class matching:
# Find the correct window_class for your app
python3 -m tuxbox.window_monitor
# Focus the application and note the window infoCommon issues:
window_classis case-insensitive but must match exactly- VSCode is
Codenotcodeorvscode - Firefox is
firefox(lowercase) - Check driver output for "Switched to profile" messages
- Verify each profile has all button definitions
- Missing buttons in profile will do nothing
- Copy all buttons from
[profile:default]then modify needed ones
- Profile mode requires a
[profile:default]section - This is the fallback when no other profile matches
- Add it even if you only have one app-specific profile
- Normal delay is 200ms (5 times per second)
- This is intentional to avoid excessive switching
- Profile switches are instant once detected
To see all available key codes:
python3 -c "from evdev import ecodes as e; print([k for k in dir(e) if k.startswith('KEY_')])"- Check the default config:
tuxbox/default_mappings.conf - See examples above
- Open an issue on GitHub with your config file and error messages
Happy customizing!