đšī¸ Using a PS3 Navigation Controller on Arch Linux
đ 2026-10-15 14:10
Everything here runs on Arch Linux with X11 and dwm:
đ§ Arch Linux Installation
đĒ Install dwm (Dynamic Window Manager)
Install the required packages
I needed to install Bluetooth utilities, Python, the Python input-device library, xdotool, evtest and triggerhappy.
# Install the required packages
sudo pacman -S bluez bluez-utils python python-evdev xdotool evtest triggerhappy
# Enable Bluetooth and start it immediately
sudo systemctl enable --now bluetooth
Setup PS3 Navigation Controller
Pair the controller
Power on the PS3 Navigation Controller and open the Bluetooth control utility:
Inside bluetoothctl, run the following commands:
# Enable the Bluetooth adapter
power on
# Enable the Bluetooth agent
agent on
# Make the agent the default
default-agent
# Search for nearby Bluetooth devices
scan on
Once the controller appears, use its Bluetooth address in place of CONTROLLER_MAC:
# Pair with the controller
pair CONTROLLER_MAC
# Trust the controller for future connections
trust CONTROLLER_MAC
# Connect to the controller
connect CONTROLLER_MAC
# Stop scanning for devices
scan off
# Exit the Bluetooth shell
exit
Give the controller a stable device path
Linux assigns input devices names such as /dev/input/event19. That number can change after reconnecting the controller or rebooting, so hardcoding it in scripts is not ideal.
Instead, create a udev rule that gives the controller a predictable symlink:
# Open a new udev rule file
sudo vim /etc/udev/rules.d/99-navigation-controller.rules
and add this rule:
# Create a stable symlink for the PS3 Navigation Controller
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="Navigation Controller", ATTRS{uniq}=="CONTROLLER_MAC", SYMLINK+="input/navigation-controller"
Reload the udev rules and apply them:
# Reload udev rules
sudo udevadm control --reload-rules
# Apply the updated rules to connected devices
sudo udevadm trigger
Turn the analog stick into a mouse
The analog stick reports its position through Linux input events. A Python script reads those events and uses xdotool to move the mouse pointer. I like keeping my custom scripts inside .config/scripts and store them inside my dotfiles repository.
Create the script:
vim ~/.config/scripts/ps3-mouse.py
Add the following code:
#!/usr/bin/env python3
# Read Linux input events
import evdev
# Run xdotool commands
import subprocess
# Wait before retrying unavailable devices
import time
# Use the stable udev symlink
DEVICE = "/dev/input/navigation-controller"
# Center position of the analog stick
CENTER_X = 131
CENTER_Y = 128
# Ignore small movements around the center
DEADZONE = 15
# Maximum number of pixels moved per update
MAX_SPEED = 8
def move_mouse(x, y):
# Calculate displacement from the stick's center
dx = x - CENTER_X
dy = y - CENTER_Y
# Ignore horizontal movement inside the deadzone
if abs(dx) <= DEADZONE:
dx = 0
# Ignore vertical movement inside the deadzone
if abs(dy) <= DEADZONE:
dy = 0
# Do nothing when the stick is centered
if dx == 0 and dy == 0:
return
# Convert stick displacement into mouse movement
mx = int(dx / (128 - DEADZONE) * MAX_SPEED)
my = int(dy / (128 - DEADZONE) * MAX_SPEED)
# Ensure small horizontal movements still move the pointer
if mx == 0 and dx:
mx = 1 if dx > 0 else -1
# Ensure small vertical movements still move the pointer
if my == 0 and dy:
my = 1 if dy > 0 else -1
# Move the pointer relative to its current position
subprocess.run(
["xdotool", "mousemove_relative", "--", str(mx), str(my)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Keep the script running across controller disconnections
while True:
# Wait until the controller device becomes available
while True:
try:
dev = evdev.InputDevice(DEVICE)
break
except (FileNotFoundError, OSError):
time.sleep(3)
# Initialize the stick position
x = CENTER_X
y = CENTER_Y
try:
# Process incoming input events
for event in dev.read_loop():
# Only handle analog-axis events
if event.type == evdev.ecodes.EV_ABS:
# Update the horizontal stick position
if event.code == evdev.ecodes.ABS_X:
x = event.value
# Update the vertical stick position
elif event.code == evdev.ecodes.ABS_Y:
y = event.value
# Move the mouse using the latest stick position
move_mouse(x, y)
except (OSError, IOError):
# Close the device after a disconnection
dev.close()
# Wait briefly before attempting to reconnect
time.sleep(1)
And make it executable with chmod +x
Now that the analog stick will act as a mouse we can
First of all edit the triggerhappy service:
sudo systemctl edit triggerhappy
And paste this:
[Unit]
After=bluetooth.target
[Service]
ExecStart=
ExecStart=/usr/sbin/thd --triggers /etc/triggerhappy/triggers.d/ --device /dev/input/navigation-controller
Restart=always
RestartSec=3
Make sure you paste the code above between:
### Editing /etc/systemd/system/triggerhappy.service.d/override.conf
### Anything between here and the comment below will become the contents of the drop-in file
### Edits below this comment will be discarded
Together, these settings make the controller listener start after bluetooth is started, use the intended configuration and input device, and recover automatically if the daemon stops. Enable the service with:
# Reload systemd unit definitions and overrides
sudo systemctl daemon-reload
# Enable triggerhappy at boot and start it now
sudo systemctl enable --now triggerhappy
The analog stick handles mouse movement, but the physical buttons are handled by triggerhappy. I chose the X button to open Netflix.com in Firefox and make it full screen and the O button to open HBOMax.com.
Netflix Script
#!/bin/sh
# Select the active X display
export DISPLAY=:0
# Tell X11 where the user's authentication file is
export XAUTHORITY=/home/sava/.Xauthority
# Connect to the user's D-Bus session
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
# Open Netflix in a new Firefox window
firefox --new-window https://www.netflix.com/ &
# Wait for Firefox to begin opening the window
sleep 1
# Toggle Firefox fullscreen
xdotool key F11
Of couse, the HBOMax one the same but the URL is changed when launching firefox.
If you want to bind a different button or simply figure out which button corresponds to which Linux input event, evtest is your friend.
# Start monitoring controller input events
sudo evtest /dev/input/navigation-controller
Now press and release the buttons you want to identify. The output will show the event name, code, and value. For example here's what happens when you press the Up arrow on the DPAD:
Event: time 1791890030.090573, -------------- SYN_REPORT ------------
Event: time 1791890030.126617, type 4 (EV_MSC), code 4 (MSC_SCAN), value 90005
Event: time 1791890030.126617, type 1 (EV_KEY), code 544 (BTN_DPAD_UP), value 0
Event: time 1791890030.126617, -------------- SYN_REPORT ------------
Event: time 1791890030.202585, type 4 (EV_MSC), code 4 (MSC_SCAN), value 90005
Event: time 1791890030.202585, type 1 (EV_KEY), code 544 (BTN_DPAD_UP), value 1
My triggerhappy conf
# Launch Netflix as the sava user when the X button is pressed
BTN_GAMEPAD 1 sudo -u sava /home/sava/.config/scripts/netflix.sh
# Launch HBO Max as the sava user when the O button is pressed
BTN_B 1 sudo -u sava /home/sava/.config/scripts/hbomax.sh
# Send Super+Q to dwm to close the focused window when the PS button is pressed
BTN_MODE 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool key super+q
# Send the media Play/Pause key when the L1 button is pressed
BTN_TL 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool key XF86AudioPlay
# Simulate a right mouse click when the L2 button is pressed
BTN_TL2 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool click 3
# Simulate a left mouse click when the analog stick is pressed (L3)
BTN_THUMBL 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool click 1
# Send the Left arrow key when the D-pad is pressed left
BTN_DPAD_LEFT 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool key Left
# Send the Right arrow key when the D-pad is pressed right
BTN_DPAD_RIGHT 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool key Right
# Send the Up arrow key when the D-pad is pressed up
BTN_DPAD_UP 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool key Up
# Send the Down arrow key when the D-pad is pressed down
BTN_DPAD_DOWN 1 DISPLAY=:0 XAUTHORITY=/home/sava/.Xauthority xdotool key Down
Finishing touches
Restart triggerhappy with:
sudo systemctl restart triggerhappy
and start the navigation controller "mouse" script at login:
~/.config/scripts/ps3-mouse.py &
exec dwm
And there you have it. If you chose to use a navigation controller yourself and use my triggerhappy config:
- pressing X launches Netflix full screen
- pressing O launches HBOMax full screen
- the analog stick is a mouse
- L1 is Play/Pause
- L2 is right mouse click
- L3 is left mouse click ( L3 means pressing down on the left analog stick until it clicks )
- the DPAD does just what it has to do: UP, DOWN, LEFT and RIGHT.
It's a fun way to repurpose old hardware and make an Arch Linux machine more comfortable to use from the couch. đī¸ + đŽ = đŊ + đŋ
đĒ Donate
If this tutorial saved you time, sanity, or a mild existential crisis... consider fueling future chaos with a small donation:
â Buy me a Ko-Fi
đ Back to my tutorials
đļ Back to my homepage