A Weird Imagination

Force focus new window immediately

The problem#

I have my window manager set to not focus new windows because I dislike having a new window pop up while typing and having the keystrokes surprisingly sent to the new window instead of the one I thought I was typing in. While this is usually what I want, this does mean extra clicks when I did mean to open the new window.

This is particularly bad for xfce4-appfinder (or any other application launcher), since the purpose to be able to set a global keyboard shortcut like Super+Space so you can press that combination and quickly type in the application or action you want (or, even better, type just the first few characters of its name). And since it's being intentionally launched by a keyboard shortcut, there's no real concern of it grabbing keyboard focus unexpectedly.

The solution#

Put the following script in a file appfinder-and-focus.sh and set the keyboard shortcut to run it instead of just running xfce4-appfinder directly:

#!/usr/bin/bash
(xprop -spy -root _NET_CLIENT_LIST | stdbuf -oL tail -n +2 |
  while read -r line
  do
    winid="${line/#*, /}"
    if [[ $(xdotool getwindowname "$winid") == \
        "Application Finder" ]]
    then
      xdotool windowactivate "$winid"
    fi
  done) 2>/dev/null &
xfce4-appfinder &

# Wait for window to appear, then kill xprop.
xdotool search --sync --name "Application Finder" >/dev/null
pkill -P "$(jobs -p %1)"

The details#

Detecting window of process#

It's tempting to start xfce4-appfinder in the background and look for windows with its PID, but that turns out not work. In order to display a window as quickly as possible, xfce4-appfinder actually just notifies a background process that it should open a window (starting that background process if necessary), so searching by PID doesn't work. Or at least would require searching for the PIDs for the other xfce4-appfinder instances. So it's simpler to just identify the window by title as it has a reasonably unique title anyway.

xdotool search --sync#

I started by looking at a previous post I had written on the similar task of reacting to the active window, although I hoped I could do something simpler as there I ended up writing a Python script instead of being able to just use existing tools from a shell script. The first iteration of that script used xdotool, so I tried to see if xdotool could do what I wanted.

Here's my first attempt:

#!/usr/bin/bash
xfce4-appfinder &
time xdotool search --sync --name "Application Finder" \
             windowactivate

It uses xdotool's search command to find a window titled "Application Finder" and runs the windowactivate command on it. The --sync option tells it to wait until such a window exists, as opposed to just exiting without doing anything if it gets to the search before xfce4-appfinder actually gets around to creating the window. And the time call is there to concretely show this is way too slow:

real    0m0.857s
user    0m0.171s
sys     0m0.073s

While it does open the window and then focus it, there's an almost one second delay, which is not great for expecting to be able to type in a search immediately. Experimenting, I can see that the delay is related to waiting for the window to open. Running the exact same command manually after the window has opened shows a much shorter delay (and then omitting --sync makes no difference):

real    0m0.157s
user    0m0.062s
sys     0m0.049s

Although even that delay isn't great. A tenth of a second is still a long time between keystrokes.

xprop -spy#

Another section of my prior blog post mentioned using xprop's -spy option to watch for changes to the X11 state, so I decided to try if that would detect changes faster than the above solution.

Running xprop -spy -root and opening some new windows, I saw the _NET_CLIENT_LIST property changed on every window opened or closed. That means that xprop -spy -root _NET_CLIENT_LIST will output a new line of text exactly when a window is opened or closed. That can be combined with while read in a shell script to take some action every time a window is opened or closed:

#!/usr/bin/bash
(xprop -spy -root _NET_CLIENT_LIST |
  while read -r _
  do
    time xdotool search --name "Application Finder" \
                 windowactivate
  done) &
xfce4-appfinder &

The action taken is the same xdotool command above (without the --sync because we don't want it to block if a different window opened). This reacts noticeably faster and the time output confirms my observations:

real    0m0.191s
user    0m0.075s
sys     0m0.041s

real    0m0.208s
user    0m0.086s
sys     0m0.021s

Skip the first call#

Ah, but that version makes two invocations of xdotool since xprop outputs one line of the current state when it opens and another line when xfce4-appfinder actually creates its window. But we know the window won't be found the by the first invocation, so it's a waste of effort (and possibly delaying the focusing of the window by up to a fifth of a second).

No problem, just add | tail -n +2 to which tells tail to output only the second line on. Unfortunately, that doesn't quite work: the script suddenly stops working at all, although suddenly actually generates outputs if I run killall xprop. Which suggests that it's waiting on output from xprop which we don't actually want to wait for. This is a common problem that I in fact ran into using xprop before, so once I realized what was happening, I just reused the solution, which was to use stdbuf:

#!/usr/bin/bash
(xprop -spy -root _NET_CLIENT_LIST | stdbuf -oL tail -n +2 |
  while read -r _
  do
    time xdotool search --name "Application Finder" \
                 windowactivate
  done) &
xfce4-appfinder &

That still results in a noticeable delay. And it involves searching through all windows, which shouldn't be necessary since we only care about newly opened windows. (Although maybe this is a sign I have too many windows open…)

I noticed the _NET_CLIENT_LIST output by xprop consistently changes at the end, so I could use it to get the window ID for the new window. Then instead of searching through all of the windows, I just had it check if the new window is the one we were looking for:

#!/usr/bin/bash
(xprop -spy -root _NET_CLIENT_LIST | stdbuf -oL tail -n +2 |
  while read -r line
  do
    winid="${line/#*, /}"
    if [[ $(time xdotool getwindowname "$winid") == \
        "Application Finder" ]]
    then
      time xdotool windowactivate "$winid"
    fi
  done) &

The result is much faster according to time, although to be fair some of the computation is now happening inside bash including the ${/#/} parameter expansion to select out just the final window ID in the list:

real    0m0.003s
user    0m0.000s
sys     0m0.002s

real    0m0.003s
user    0m0.002s
sys     0m0.000s

More importantly, it feels immediate. If I mash on the keyboard while pressing Super+Space, I can still manage to get some keypresses sent to the current application first, but at any reasonable typing speed, the window has been focused before I can press down another key.

Killing xprop#

One notable omission is that the scripts above all leave the xprop loop running, even though it's already done its job. I tried adding exit or break after the call to xdotool to focus the window, but those didn't actually make it exit. As a workaround, I realized I could just use xdotool's search --sync feature to wait for the window to appear and kill the child job after, assuming it's gotten around to focusing the window by then:

# Wait for window to appear, then kill xprop.
xdotool search --sync --name "Application Finder" >/dev/null
pkill -P "$(jobs -p %1)"

This really feels like a hack that should be unnecessary that I would like to get rid of in a future post.

Final cleanup#

The final script listed in the solution section of this post has the usages of time removed and redirects the output to /dev/null so it doesn't print anything to the terminal. Not that it really matters for a script that's not going to be run from a terminal anyway.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.