Cat-Proofing Your Screen Locker with Bash

cat walking on computer

 

I have a computer in my bedroom. I also have cats. Unfortunately, cats and screen lockers don't mix well, particularly at night. To be accurate, it's more a problem with the display power management than the actual screen locking. Here's the way it works: I run a script to "shut the lights off at night" (that is, lock the screen and force the display to power down), and that works great, until one of the cats jumps on the desk and causes the mouse to move and turn the display back on. And the cats don't even have to touch the mouse; the slight movement of the desk is enough to cause the mouse to react. Recently, I'd had enough of it and figured there had to be a way to disable the mouse and "refactor" the script.

The initial version of the script is pretty simple:

qdbus org.kde.screensaver /ScreenSaver Lock
sleep 2
xset +dpms
xset dpms force off

The first line locks the screen. The third line makes sure that display power management is enabled, and the last line powers down the display.

If you use GNOME, the qdbus line may not work, so try the following instead:

dbus-send --type=method_call --dest=org.gnome.ScreenSaver \
    /org/gnome/ScreenSaver org.gnome.ScreenSaver.Lock

Turning off the mouse is also simple to do using the xinput command (which you may need to install):

xinput --set-prop "Your-Mouse-Name" "Device Enabled" "0"

The somewhat tricky part is figuring out what to use for "Your-Mouse-Name". This too is fairly straightforward using the xinput command again:

$ xinput list
⎡ Virtual core pointer                          id=2    [master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
⎜   ↳ HOLTEK USB-HID Keyboard Mouse             id=10   [slave  pointer  (2)]
⎜   ↳ HOLTEK USB-HID Keyboard Consumer Control  id=12   [slave  pointer  (2)]
⎜   ↳   mini keyboard Mouse                     id=16   [slave  pointer  (2)]
⎜   ↳   mini keyboard Consumer Control          id=18   [slave  pointer  (2)]
⎜   ↳ USB Mouse                                 id=14   [slave  pointer  (2)]
⎣ Virtual core keyboard                         id=3    [master keyboard (2)]
    ↳ Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    ↳ Power Button                              id=6    [slave  keyboard (3)]
    ↳ Video Bus                                 id=7    [slave  keyboard (3)]
    ↳ Power Button                              id=8    [slave  keyboard (3)]
    ↳ HOLTEK USB-HID Keyboard                   id=9    [slave  keyboard (3)]
    ↳ HOLTEK USB-HID Keyboard System Control    id=11   [slave  keyboard (3)]
    ↳ HOLTEK USB-HID Keyboard Keyboard          id=13   [slave  keyboard (3)]
    ↳   mini keyboard                           id=15   [slave  keyboard (3)]
    ↳   mini keyboard System Control            id=17   [slave  keyboard (3)]
    ↳ HOLTEK USB-HID Keyboard Consumer Control  id=19   [slave  keyboard (3)]
    ↳   mini keyboard Consumer Control          id=20   [slave  keyboard (3)]

Okay, maybe it's not that straightforward, since on my system there are three things that are named "Mouse". But I knew that it wasn't the first two, since their names gave them away as being part of the keyboard, leaving me with just the universal-sounding "USB Mouse".

Unfortunately, when I tried this on another system with a different type of USB mouse, the mouse name did not show up as just "USB Mouse", it came up as "Logitech USB Laser Mouse", which meant I the couldn't hard-code the mouse name into the script. After looking at the two lists a bit, I noticed that the mouse name that I wanted was always the name right before the "Virtual core keyboard" line. Admittedly, my assumption is based on a pretty small test group, but until I see something different, it'll work. Note: as I write this, it occurs to me that maybe I could have just disabled the "Virtual core pointer", which likely is a "universal" name, but I haven't tried that yet.

So with a bit of grepping, I can now get the name of the mouse on any system:

mouse_name=$(xinput list --name-only | grep -B 1 'Virtual core keyboard' | head -n 1)

The --name-only option to xinput list eliminates all the fancy arrows and the other extra information and just outputs names.

The -B option to grep causes grep to output preceding lines of context for any matches that it finds. In this case, in addition to the "Virtual core keyboard" line, it outputs the line right before it (which is the one I want). Then I use head to pick off that line, and that gives me the name of the mouse to shut off ("USB Mouse" in the example above).

Tip: Grep Context Options

Grep's context options can come in quite handy sometimes, from the grep man page:

-A NUM, --after-context=NUM

Print NUM lines of trailing context after matching lines. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given.

-B NUM, --before-context=NUM

Print NUM lines of leading context before matching lines. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given.

-C NUM, -NUM, --context=NUM

Print NUM lines of output context. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given.

The entire mouse-on-off script follows:

#!/bin/bash

function usage()
{
    cat <<EOF
Usage: $0 MOUSE-STATE

MOUSE-STATE:
    on|enable|enabled|1     Enable mouse
    off|disable|disabled|0  Disable mouse
    test|mouse              Display mouse name
EOF
}

if test "$DISPLAY"; then
    mouse_name=$(xinput list --name-only | grep -B 1 'Virtual core keyboard' | head -n 1)

    if [[ "$1" =~ ^(on|enable|enabled|1)$ ]]; then
        xinput --set-prop "$mouse_name" "Device Enabled" "1"
        echo "on"
    elif [[ "$mouse_name"  &&  "$1" =~ ^(off|disable|disabled|0)$ ]]; then
        xinput --set-prop "$mouse_name" "Device Enabled" "0"
        echo "off"
    elif [[ "$1" =~ ^(test|mouse)$ ]]; then
        echo "Mouse name: ${mouse_name}"
    else
        echo "Mouse name: ${mouse_name}" >&2
        usage >&2
    fi
else
    echo "Mouse name: UNKNOWN (No X server ???)"
    if [[ "$1" =~ ^(on|enable|enabled|1|off|disable|disabled|0)$ ]]; then
        echo "on"
    elif [[ "$1" =~ ^(test|mouse)$ ]]; then
        :
    else
        usage >&2
    fi
fi

Some examples of using it:

$ mouse-on-off mouse      # Show mouse name
Mouse name: USB Mouse
$ mouse-on-off off        # Turn off mouse
off
$ mouse-on-off on         # Turn on mouse
on

Examples of things going wrong:

$ mouse-on-off mouse      # Unable to get mouse name
Mouse name: UNKNOWN (No X server ???)
$ mouse-on-off off        # Unable to turn off mouse
Mouse name: UNKNOWN (No X server ???)
on                        # Mouse is still on

Now all I need to do is modify the original script to use the mouse-on-off command:

#!/bin/bash

qdbus org.kde.screensaver /ScreenSaver Lock
sleep 2

mouse=$(mouse-on-off off)

xset +dpms
xset dpms force off

if [[ "$mouse" == 'off' ]]; then
    kdialog --msgbox "Enter to re-enable the mouse"
    mouse-on-off on
fi

The mouse=$(mouse-on-off off) invokes the new command and captures its standard output, which should be either "on" or "off" (the error messages go to standard error). After shutting off the display, assuming that I was able to shut off the mouse, I put up a message box. When I unlock the computer, I just press Enter to dismiss the message box that greets me, and the script turns the mouse back on and exits. And in case you're wondering, yes, for the first few days of using this, I kept thinking the mouse had died during the night [insert cat/mouse joke here] when I wiggled it in the morning to try to wake up the monitors. Old habits die hard, but they do die, eventually.

Load Disqus comments