0

UPDATE - Fully working script located at the bottom of my answer for anyone who isn't interested in the process of getting there.

I've been trying to write a bash script that makes use of xbindkeys, xkb, and xinput set-button-map in order to automatically change the layouts of my Razer Tartarus keypad and Logitech G502 Proteus mouse based on the currently active window. I've chosen to accomplish this by having the script constantly running in the background in an infinite loop that checks to see if the active window is different from the last check. I've seen others suggest to have your .xbindkeysrc run a different script for each key/key-combination that checks the active window before deciding which command to send, but with a 13-button mouse and a 21-button keypad, the number of necessary scripts would quickly get out of hand, especially once I start adding combinations.

autoProfileSwitch:

#!/bin/bash

Last=""

proteus_id=$(
    xinput list | 
    sed -n 's/.*G502.*id=\([0-9]*\).*pointer.*/\1/p'
)
[ "$proteus_id" ] || exit

tartarus_id=$(
    xinput list |
    sed -n 's/.*Tartarus.*id=\([0-9]*\).*keyboard.*/\1/p'
)
[ "$tartarus_id" ] || exit

tartarus_profile="default"
proteus_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
xbindkeys_profile=".xbindkeysrc"

while true; do
  Class=`xprop -id \`xprop -root |nawk '/_NET_ACTIVE_WINDOW/ {print $5;       exit;}'\` |nawk -F = '/WM_CLASS/ {N=split($2, A, ", ");  gsub(/\"/,"",A[2]); print A[2]; exit;}'`

  if [ "$Class" != "$Last" ]
  then

    case $Class in
        "Dwarf_Fortress")   
            tartarus_profile="dwarfFortress"
            proteus_profile="1 3 2 4 5 6 7 8 9 10 11 12 13"
            xbindkeys_profile="dwarfFortress";;

        "Firefox")          
            tartarus_profile="default"
            proteus_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
            xbindkeys_profile=".xbindkeysrc";;

        "")                 
            tartarus_profile="default"
            proteus_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
            xbindkeys_profile=".xbindkeysrc";;

        *)                  
            tartarus_profile="default"
            proteus_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
            xbindkeys_profile=".xbindkeysrc";;
    esac

    if pgrep -x "xbindkeys" > /dev/null
    then
        killall xbindkeys
    fi

    xbindkeys -f $HOME/xbindkeys\ profiles/$xbindkeys_profile

    tartarusProfile -p $tartarus_profile
    #setxkbmap -device $tartarus_id -print | 
        #sed 's/\(xkb_symbols.*\)"/\1+tartarus('$tartarus_profile')"/' | 
        #xkbcomp -I$HOME/xbindkeys\ profiles/xkb -i $tartarus_id -synch -$DISPLAY 2>/dev/null

    for i in $proteus_id
        do
            xinput set-button-map $i $proteus_profile
        done

    Last="$Class"
  fi

done

I tried moving the contents of my tartarusProfile script into the autoProfileSwitch script (the reason for the unused tartarus_id variable and the commented out lines directly under the call to tartarusProfile), but kept receiving a "sed couldn't flush stdout: Broken pipe" error for some reason. The code works fine when in its own script

tartarusProfile:

#!/bin/bash

# Set profile variable to argument (or default if none)

PROFILE="default"

while getopts p: option; do
    case "$option" in
        p) PROFILE=$OPTARG;;
    esac
done

# Get xinput device id for Razer Tartarus

tartarus_id=$(
    xinput list |
    sed -n 's/.*Tartarus.*id=\([0-9]*\).*keyboard.*/\1/p'
)
[ "$tartarus_id" ] || exit

# Remap Razer Tartarus to selected profile

setxkbmap -device $tartarus_id -print | 
 sed 's/\(xkb_symbols.*\)"/\1+tartarus('$PROFILE')"/' | 
 xkbcomp -I$HOME/xbindkeys\ profiles/xkb -i $tartarus_id -synch - $DISPLAY 2>/dev/null

These scripts are mostly working as intended, but there are some strange things going on that I can't seem to isolate. For starters: the xkb calls only seem to happen if a terminal window is open and not minimized (or in one more instance which I'll describe later), even when I run the script with "autoProfileSwitch &"; xbindkeys and xinput are both called regardless of if a terminal window is open however.

Another issue is that the "Tab" key is occasionally bound to my right mouse button (in addition to the intended swapping of MMB and RMB) when switching to my Dwarf Fortress profiles despite there being nothing in the xbindkeys profile to cause it.

Finally: even though xbindkeys doesn't require the terminal window be open, something weird is happening when I go from Dwarf Fortress to another window when it's closed. Right now my default is to have my mouse's G7 button bound to the "f" key to allow quick fullscreen on videos, but when I switch from Dwarf Fortress to another window, the first click of G7 gives an "s" (nothing in xbindkeys that should be causing this), and the second click gives the expected "f". After G7 becomes my "f" key, xkb gets called and my tartarus then switches to its default as well. This tends to keep xkb from switching back to the Dwarf Fortress profile when appropriate, even when the terminal window is open.

Any help would be much appreciated, and I can provide more info if necessary.

1
  • Edit out the answer and post it as answer because this is a question, not an answer
    – random
    Commented Oct 30, 2017 at 2:52

1 Answer 1

0

After further testing, I've narrowed the problem down to this section of code:

setxkbmap -device $tartarus_id -print | 
    sed 's/\(xkb_symbols.*\)"/\1+tartarus('$tartarus_profile')"/' | 
    xkbcomp -I$HOME/xbindkeys\ profiles/xkb -i $tartarus_id -synch - $DISPLAY 2>/dev/null

My earlier problem with the broken pipe error was due to a typo wherein I had accidentally deleted the space between - and $DISPLAY, so this code now works within the main script. The problem is that this section of code is temporarily screwing up my xbindkeys config, and the new keyboard layout only takes effect after xbindkeys straightens itself back out. Xbindkeys only manages to straighten itself out after I click a mouse button that it has rebound, but there is always something wrong with that first click. I know that this is the code that's causing the issues because I've commented out everything else, and the problem still persists even when the xkb profile is the only thing being changed.

UPDATE 1: Even though I had been doing everything in my power to avoid it, I eventually caved and took the time to figure out how to add layouts to xkb by reading this answer such that I can now call

setxkbmap -device $tartarus_id -layout tartarus -variant $tartarus_profile

This has solved one of my two remaining problems: My Tartarus successfully rebinds as soon as the active window changes. My mouse is still behaving bizarrely though.

It's odd. The whole "G7 button sending 's' the first click" problem seems to have stopped, but "Tab" is still mapping itself onto my RMB and G9 button for the first click in Dwarf Fortress. I haven't noticed any other irregularities, but I haven't exactly added a bunch of profiles yet to see how this continues to play out. Whatever is happening, it seems to be related to buttons on my mouse being configured to send keystrokes that are the same as keys that have changed on the Tartarus.

In any case, the script is working well enough that I'm willing to actually start using it. If it's just the first click of any particular mouse button that is wrong (and not every button mind you), that's something I can live with; if my mouse problem gets fixed in the future, that's just a bonus.

UPDATE 2: While troubeshooting the strange behavior of my mouse, I rearranged the order of commands so that xbindkeys is not running while setxkbmap is making changes. I've also tried switching from xte to xdotool when sending keys and removing "+ Release" from problem buttons in my xbindkeys configs. The behavior has become less frequent, but still happens occasionally.

On an unrelated note, using xprop to get the active window class was giving me some problems with fullscreen video, so I switched to using xdotool to get the active window's name, which honestly is much more readable anyways and seems to be working perfectly.

UPDATE 3: Moved the check for mouse and keypad IDs into the main loop and used whether or not they've been found to determine whether or not to change their profiles. As a result, the script can now handle mice and keypads being connected/disconnected while running.

UPDATE 4: Turned out that the strange behavior from xbindkeys wasn't actually fixed at all. Long story short: it's a problem with both xte and xdotool freaking out when multiple keyboards with different layouts are connected. Found a workaround for anyone experiencing this problem: add xdotool key Cancel && to the beginning of every macro line that calls xdotool (I assume this works for xte too, but I haven't tried it). For example:

"xdotool key Return"
b:10

becomes

"xdotool key Cancel && xdotool key Return"
b:10

Doing this causes the first call of xdotool (which is usually wrong) to send a "dead" key, while the second call gives you what you actually wanted.

Here's the up-to-date autoProfileSwitch script for anyone who wants to give this a shot for their own mouse and keypad combos. Do me a favor and upvote this question if you find this script useful:

#!/bin/bash

# Set Mouse and Keypad Names
# Edit These To Uniquely Identify Your Mouse and Keyboard in Xinput Output
# Leading and Trailing Wildcard Characters Not Necessary For Partial Names
mouse_name="YourMouseNameHere"
keypad_name="YourKeypadNameHere"

# Location of Xbindkeys Configuration Files
xbindkeys_dir="$HOME/xbindkeys profiles"

# Set Initialization Profiles
# keypad_layout Is an XKB Symbols File
# keypad_profile Is an XKB Symbols Definition within keypad_layout
# mouse_profile Is an xinput set-button-map Button Map String
# macro_profile Is an Xbindkeys Configuration File
keypad_layout="tartarus"
keypad_profile="default"
mouse_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
macro_profile=".xbindkeysrc"

# Keep Track of Last Time Active Window Changed
Last=""

# Main Loop
while true; do

  # Get Mouse and Keypad Xinput IDs
  mouse_id=$(
    xinput list | 
    sed -n 's/.*'$mouse_name'.*id=\([0-9]*\).*pointer.*/\1/p'
  )

  keypad_id=$(
    xinput list |
    sed -n 's/.*'$keypad_name'.*id=\([0-9]*\).*keyboard.*/\1/p'
  )

  # Get Name of Active Window
  Name="$(xdotool getwindowfocus getwindowname)"

  # Execute if Currently Active Window is Different from the Last Time It Changed
  if [ "$Name" != "$Last" ]
  then

    # Set Profiles Based on Name of Currently Active Window
    case $Name in
        Dwarf\ Fortress)   
            keypad_profile="dwarfFortress"
            mouse_profile="1 3 2 4 5 6 7 8 9 10 11 12 13"
            macro_profile="dwarfFortress";;

        *Firefox)          
            keypad_profile="blankSlate"
            mouse_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
            macro_profile="firefox";;

        *)                  
            keypad_profile="default"
            mouse_profile="1 2 3 4 5 6 7 8 9 10 11 12 13"
            macro_profile=".xbindkeysrc";;
    esac

    # Kill Xbindkeys
    if pgrep -x "xbindkeys" 1>/dev/null
    then
        killall xbindkeys
    fi  

    # Change Keypad Keymap to Appropriate Profile
    # Layout Is the Name of Your XKB Symbols File
    # Variant Is the Name of an xkb_symbols Section of the Layout File
    # Save Layout to /usr/share/X11/xkb/symbols/ 
    # Modify /usr/share/X11/xkb/rules/evdev .../evdev.xml and .../evdev.lst to Include Your Layout
    # Run "sudo dpkg-reconfigure xkb-data" After Any Changes to xkb/ Directory    
    # See https://askubuntu.com/a/483026 For More Info
    [ ! "$keypad_id" ] || setxkbmap -device $keypad_id -layout $keypad_layout -variant $keypad_profile

    # Restart Xbindkeys Using Appropriate Profile
    xbindkeys -f "$xbindkeys_dir"/$macro_profile

    # Set Mouse Profile
    # For When Your Device Appears More Than Once in Xinput
    [ ! "$mouse_id" ] || for i in $mouse_id; do xinput set-button-map $i $mouse_profile; done

    # This Is the Last Time The Active Window Changed
    Last="$Name"
  fi

# Short Sleep to Minimize CPU Usage
sleep .5

done

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .