Bash: Interactive Script to Bring Window to Front or Open App

Ever find yourself rotating between multiple applications within the same single workflow? For me, I like to call it the holy trinity: VS Code, GitKraken and Firefox. When I'm working on a site, I'll inevitably spend almost as much time cycling from one of these applications to the other as I do writing code. I finally got so sick and tired of this that I decided to write a little Bash script to make rotating between applications just a little less cumbersome. Since VS Code has a terminal emulator, I can run the script from there to jump quickly to the next application(s) in my workflow.

Background: Multitasking Wears You Down!

Web development is a conglomeration of tasks; there's really no way around it. Having two monitors is a good way to get a bird's-eye view of everything going on within your workflow. If you like to maximize your windows, though, you're only ever going to get two applications to work with at any given time. On an average day, I'll have roughly 3 Firefox windows, a terminal, VS Code (with its own terminal emulator), GitKraken, Koala (SASS compiler) and maybe Virtual Box--all open at the same time. Nah--let's throw in GIMP and Atril, too--just for good measure. That's a lot of real estate to stuff into my XFCE Panel. Squinting at the Panel as I try and figure out which of my Firefox windows is the one with the dev-site open can get tiring really fast. I spend just enough time on the terminal, too, that reaching for the mouse and stumbling to the bottom of the screen just to get my Panel to pop open (I keep it hidden) has also become a drag. There's gotta be a better way!

Conceptualizing the Scripts

So, that's where I'm coming from: I'm starting from a terminal (either XFCE4 or VS Code), and I want a script that can bring an application window to the front without me having to grab my mouse and look for it in the Panel; if the application isn't running, I want it to to start the application; lastly, in the case of my browser, Firefox, I want the script to report window titles and let me choose which one to bring forward (I need this since I usually have multiple browser windows open). That's what I'm after. I won't cover it in this post, but I'll also bind a few scripts to key combinations so that I can easily find my way back to either the terminal or VS Code (look for a separate post on this).

Dependencies

If you'd like to use this script, you'll need to have the following installed:

  • xwininfo (sudo apt install x11-utils -- although I think it comes standard with distros using Xorg...)
  • wmctrl (sudo apt install wmctrl)
  • xdotool (sudo apt install xdotool)
  • lolcat (sudo apt install lolcat) -- don't actually need, but it's useful for calling attention to prompts.

The Script

#! /bin/bash
# filename: get.sh
# Script to determine whether an application is running; if running bring the window to the front
# otherwise open the application independent of the terminal. This only works with applications
# that can be called from the command line.
#
# In the case of Firefox, I often have multiple windows open. In this case, the script will prompt
# you to select which of the open windows you'd like to bring to the front.
#
# $1 arg is the name of the application you'd like to fetch.
#
# Usage example:
# ./get.sh firefox
# ./get.sh gitkraken
if [ "$1" == ff ]; then
  alias='firefox'
elif [ "$1" == gk ]; then
  alias='gitkraken'
elif [ "$1" == vs ]; then
  alias='code'
elif [ "$1" == t ]; then
  alias='terminal'
else
  alias="$1"
fi

# grep xwininfo to determine whether an application has a window open
isopen="$(xwininfo -root -children | grep -i $alias)"

# use wmctrl to find open FF windows
ff="$(wmctrl -l | grep Firefox)"

# determine if more than one window is open;
# will return empty string if $ff is only one line
multi="$(echo "$ff" | sed -n '2 p')"

# parse down to window name only
all="$(echo "$ff" | cut -b 27-)"

# and number the output
list="$(echo "$all" | nl)"

# if FF is requested, is running, and has multiple windows open
if [ "$1" == ff ] && [ "$isopen" != "" ] && [ "$multi" != "" ]; then

  # prompt to select from open windows
  echo "Please select a window to open:" | lolcat
  echo "$list"
  read -p "Enter a number: " num

  # search list for num with grep and parse again
  window="$(echo "$list" | grep $num | cut -b 8-)"

  # use parsed window name to get a window ID with xdotool
  id="$(xdotool search --name ''"$window"'')"

  # BAM!
  xdotool windowactivate "$id"

else

  # if application has a win open, bring to front (also redirect STDERR ouput to /dev/null). Else, open the requested app
  [ "$isopen" != "" ] && xdotool search --desktop 0 $alias windowactivate 2> /dev/null || $alias </dev/null &>/dev/null &

fi

Breakdown: Aliases

In the end, we'll use xdotool to handle window management; for applications that aren't yet running, we can just call them from the script the same we would from the command prompt. In either case, we can specify which application we're looking for by simply passing it's name to the script as an argument. If I had to type out 'gitkraken' every time I wanted to bring focus to it, though, I'd probably wind up with hemorrhoids. Instead, I'll just create some aliases. Instead of passing 'gitkraken' as an argument, I'll just pass 'gk'; instead of 'firefox', I'll just use 'ff'. This first section is just to alias a few of my most used applications. I'll use the $alias variable throughout the rest of the script, so for any arguments not present in this list, the $alias variable is equal to $1:  

if [ "$1" == ff ]; then
  alias='firefox'
elif [ "$1" == gk ]; then
  alias='gitkraken'
elif [ "$1" == vs ]; then
  alias='code'
elif [ "$1" == t ]; then
  alias='terminal'
else
  alias="$1"
fi

Determine if Requested App Has Window Open

The next step is to figure out whether the requested app has a window open (i.e.; it's running). For this, we can use xwininfo and pipe the output to grep; with grep we search the output for whatever alias was given to the script as an argument above. If the application has a window open, xwininfo and grep will provide an output; likewise, if the application doesn't have a window open (i.e.; it's not running), the command won't give any output. We'll store the output (or lack of output) in a variable, $isopen, that we can then use to judge whether we should bring a window to the front or simply open the application.

# grep xwininfo to determine whether an application has a window open
# -i option with grep ignores case: i.e., GitKraken or gitkraken
isopen="$(xwininfo -root -children | grep -i $alias)"

Preparing & Parsing for Firefox Logic

There's a pretty good chance that Firefox has multiple windows open, so we need to do some parsing of window names so that we can get window ID's and then let the script call for a specific window. The first line below uses wmctrl to list all window name and pipes the output to grep. grep will bring back only Firefox windows--this output is stored in the $ff variable. Next we need to look at $ff; if the output is more than one line, we know that we have multiple windows open. I determine this by piping the output of $ff to sed. sed looks only at the second line of the $ff output. If Firefox only has one window open, sed will find nothing on the second line. The value of the second line, or lack of value, is stored in the $multi variable. We can use that variable later to trigger a prompt allowing us to select from which window we'd like to call. The third line parses the window name output down to just the window names using cut and stores it in another variable (you might want to double check that cut is removing the correct number of bytes for your shell -- you can test in your terminal with the following: $ wmctrl -l | grep Firefox | cut -b 27-). That variable gets piped to nl in the last line which and placed in another variable that we can echo to the user when we prompt them to select from the multiple windows:

# use wmctrl to find open FF windows
ff="$(wmctrl -l | grep Firefox)"

# determine if more than one window is open;
# will return empty string if $ff is only one line
multi="$(echo "$ff" | sed -n '2 p')"

# parse down to window name only
# maybe double check that cut is removing the correct number of bytes for your shell
all="$(echo "$ff" | cut -b 27-)"

# and number the output
list="$(echo "$all" | nl)"

If Firefox Is Called

On to the meat and potatoes: we need to test whether Firefox has been called, whether it's already open, and whether it has multiple windows open. If yes, we'll follow up on a lot of the parsing we did in the step above. We'll prompt the user to choose a window from our numbered output ($list). The number they enter is stored in a variable, $num; we can then pipe the output of $list to grep and use $num to get the name of the requested window. We use cut once again to parse down to just the window name. The bare window name gets stored in another variable: $window. We can then pass the $window variable to xdotool to get the id of the window. Lastly, xdotool can use the $id to bring the requested window to the front.

# if FF is requested, is running, and has multiple windows open
if [ "$1" == ff ] && [ "$isopen" != "" ] && [ "$multi" != "" ]; then

  # prompt to select from open windows
  echo "Please select a window to open:" | lolcat
  echo "$list"
  read -p "Enter a number: " num

  # search list for num with grep and parse again
  window="$(echo "$list" | grep $num | cut -b 8-)"

  # use parsed window name to get a window ID with xdotool
  id="$(xdotool search --name ''"$window"'')"

  # BAM!
  xdotool windowactivate "$id"

If Any Other Application Was Called

For all other applications, we can simply test whether they already have a window open. If so, we can use xdotool to bring them to the front with passing their value in $alias. If not, we can just call them the same as we would from the command prompt: 


else

  # if application has a win open, bring to front (also redirect STDERR ouput to /dev/null). Else, open the requested app
  [ "$isopen" != "" ] && xdotool search --desktop 0 $alias windowactivate 2> /dev/null || $alias </dev/null &>/dev/null &
  exit

fi

And, that should be about it.

Again, this script assumes that the terminal (or, a terminal) is the starting point for your workflow. When I'm working with Git in VS Code and I need to jump to another application (i.e.; GitKraken to resolve a merge conflict, or Firefox to proof a code push), this script really shines. If you spend enough time on the command line, at a certain point, the mouse becomes a bit of hindrance: your brain needs to switch modalities, and it can be a bit jarring. You'll may still need the mouse once the script has handled juggling applications, but at the very least it'll buy you a few more seconds to transition to mouse-brain--you won't have to squint at your Panel either.

Ideally, you can setup a few scripts binded to keys to bring you back to your terminal once you're ready. This is pretty easy to do, so I'll see if I can squeeze out a post on it in the near future. Otherwise, enjoy!