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 &
No need to search#
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.